Browse Source
* Added PipsPager control * Added tests * Added render tests * Added samples * More improvements * More tests * Added more samples * Fix formatting * Updated Automation * Small optimization * More changes * Changes based on feedback * Fix build errors * More changes * Updated samples * Fixes * More changes * Fix build * More changes * More changes * More tests * More constantspull/19830/merge
committed by
GitHub
29 changed files with 2680 additions and 0 deletions
@ -0,0 +1,50 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="ControlCatalog.Pages.PipsPagerCarouselPage"> |
||||
|
<DockPanel> |
||||
|
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
||||
|
<StackPanel Margin="12" Spacing="8"> |
||||
|
<TextBlock Text="Carousel Integration" FontSize="16" FontWeight="SemiBold" |
||||
|
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
||||
|
Text="Bind SelectedPageIndex to a Carousel's SelectedIndex for two-way synchronized page navigation." /> |
||||
|
<Separator /> |
||||
|
<TextBlock Text="Binding" FontSize="13" FontWeight="SemiBold" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" |
||||
|
Text="SelectedIndex='{Binding #Pager.SelectedPageIndex, Mode=TwoWay}'" /> |
||||
|
</StackPanel> |
||||
|
</ScrollViewer> |
||||
|
|
||||
|
<Border DockPanel.Dock="Right" Width="1" |
||||
|
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
||||
|
|
||||
|
<Grid RowDefinitions="*,Auto" Margin="24"> |
||||
|
<Carousel Name="GalleryCarousel" |
||||
|
SelectedIndex="{Binding #GalleryPager.SelectedPageIndex, Mode=TwoWay}"> |
||||
|
<Carousel.Items> |
||||
|
<Border Background="#E3F2FD" CornerRadius="8"> |
||||
|
<TextBlock Text="Page 1" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" /> |
||||
|
</Border> |
||||
|
<Border Background="#C8E6C9" CornerRadius="8"> |
||||
|
<TextBlock Text="Page 2" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" /> |
||||
|
</Border> |
||||
|
<Border Background="#FFE0B2" CornerRadius="8"> |
||||
|
<TextBlock Text="Page 3" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" /> |
||||
|
</Border> |
||||
|
<Border Background="#E1BEE7" CornerRadius="8"> |
||||
|
<TextBlock Text="Page 4" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" /> |
||||
|
</Border> |
||||
|
<Border Background="#FFCDD2" CornerRadius="8"> |
||||
|
<TextBlock Text="Page 5" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="30" Opacity="0.6" /> |
||||
|
</Border> |
||||
|
</Carousel.Items> |
||||
|
</Carousel> |
||||
|
|
||||
|
<PipsPager Name="GalleryPager" |
||||
|
Grid.Row="1" |
||||
|
NumberOfPages="5" |
||||
|
HorizontalAlignment="Center" |
||||
|
Margin="0,12,0,0" /> |
||||
|
</Grid> |
||||
|
</DockPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace ControlCatalog.Pages; |
||||
|
|
||||
|
public partial class PipsPagerCarouselPage : UserControl |
||||
|
{ |
||||
|
public PipsPagerCarouselPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,78 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="ControlCatalog.Pages.PipsPagerCustomButtonsPage"> |
||||
|
<DockPanel> |
||||
|
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
||||
|
<StackPanel Margin="12" Spacing="8"> |
||||
|
<TextBlock Text="Custom Buttons" FontSize="16" FontWeight="SemiBold" |
||||
|
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
||||
|
Text="Replace the default chevron navigation buttons with custom styled buttons using PreviousButtonStyle and NextButtonStyle." /> |
||||
|
<Separator /> |
||||
|
<TextBlock Text="Properties" FontSize="13" FontWeight="SemiBold" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="PreviousButtonStyle" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="NextButtonStyle" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="IsPreviousButtonVisible" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="IsNextButtonVisible" /> |
||||
|
</StackPanel> |
||||
|
</ScrollViewer> |
||||
|
|
||||
|
<Border DockPanel.Dock="Right" Width="1" |
||||
|
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
||||
|
|
||||
|
<StackPanel Spacing="24" Margin="24"> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Text Buttons" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="5" MaxVisiblePips="5" |
||||
|
IsPreviousButtonVisible="True" IsNextButtonVisible="True"> |
||||
|
<PipsPager.Resources> |
||||
|
<ControlTheme x:Key="CustomPreviousButtonStyle" TargetType="Button"> |
||||
|
<Setter Property="Content" Value="Prev" /> |
||||
|
<Setter Property="Background" Value="LightGray" /> |
||||
|
<Setter Property="Foreground" Value="Black" /> |
||||
|
<Setter Property="Padding" Value="8,2" /> |
||||
|
<Setter Property="Margin" Value="0,0,8,0" /> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<Border Background="{TemplateBinding Background}" CornerRadius="4"> |
||||
|
<ContentPresenter Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" /> |
||||
|
</Border> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</ControlTheme> |
||||
|
<ControlTheme x:Key="CustomNextButtonStyle" TargetType="Button"> |
||||
|
<Setter Property="Content" Value="Next" /> |
||||
|
<Setter Property="Background" Value="LightGray" /> |
||||
|
<Setter Property="Foreground" Value="Black" /> |
||||
|
<Setter Property="Padding" Value="8,2" /> |
||||
|
<Setter Property="Margin" Value="8,0,0,0" /> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<Border Background="{TemplateBinding Background}" CornerRadius="4"> |
||||
|
<ContentPresenter Content="{TemplateBinding Content}" Margin="{TemplateBinding Padding}" /> |
||||
|
</Border> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</ControlTheme> |
||||
|
</PipsPager.Resources> |
||||
|
<PipsPager.PreviousButtonStyle> |
||||
|
<StaticResource ResourceKey="CustomPreviousButtonStyle" /> |
||||
|
</PipsPager.PreviousButtonStyle> |
||||
|
<PipsPager.NextButtonStyle> |
||||
|
<StaticResource ResourceKey="CustomNextButtonStyle" /> |
||||
|
</PipsPager.NextButtonStyle> |
||||
|
</PipsPager> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Hidden Buttons" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="7" |
||||
|
MaxVisiblePips="7" |
||||
|
IsPreviousButtonVisible="False" |
||||
|
IsNextButtonVisible="False" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
</StackPanel> |
||||
|
</DockPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace ControlCatalog.Pages; |
||||
|
|
||||
|
public partial class PipsPagerCustomButtonsPage : UserControl |
||||
|
{ |
||||
|
public PipsPagerCustomButtonsPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,59 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="ControlCatalog.Pages.PipsPagerCustomColorsPage"> |
||||
|
<DockPanel> |
||||
|
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
||||
|
<StackPanel Margin="12" Spacing="8"> |
||||
|
<TextBlock Text="Custom Colors" FontSize="16" FontWeight="SemiBold" |
||||
|
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
||||
|
Text="Override pip indicator colors using resource keys." /> |
||||
|
<Separator /> |
||||
|
<TextBlock Text="Resource Keys" FontSize="13" FontWeight="SemiBold" /> |
||||
|
<TextBlock FontSize="11" TextWrapping="Wrap" Text="PipsPagerSelectionIndicatorForeground" /> |
||||
|
<TextBlock FontSize="11" TextWrapping="Wrap" Text="PipsPagerSelectionIndicatorForegroundSelected" /> |
||||
|
<TextBlock FontSize="11" TextWrapping="Wrap" Text="PipsPagerSelectionIndicatorForegroundPointerOver" /> |
||||
|
</StackPanel> |
||||
|
</ScrollViewer> |
||||
|
|
||||
|
<Border DockPanel.Dock="Right" Width="1" |
||||
|
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
||||
|
|
||||
|
<StackPanel Spacing="24" Margin="24"> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Orange / Blue" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="5" MaxVisiblePips="5"> |
||||
|
<PipsPager.Resources> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="Orange" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="Blue" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="Gold" /> |
||||
|
</PipsPager.Resources> |
||||
|
</PipsPager> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Green / Red" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="5" MaxVisiblePips="5"> |
||||
|
<PipsPager.Resources> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="#81C784" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="#E53935" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="#A5D6A7" /> |
||||
|
</PipsPager.Resources> |
||||
|
</PipsPager> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Purple / Teal" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="5" MaxVisiblePips="5"> |
||||
|
<PipsPager.Resources> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="#CE93D8" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="#00897B" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="#BA68C8" /> |
||||
|
</PipsPager.Resources> |
||||
|
</PipsPager> |
||||
|
</StackPanel> |
||||
|
|
||||
|
</StackPanel> |
||||
|
</DockPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace ControlCatalog.Pages; |
||||
|
|
||||
|
public partial class PipsPagerCustomColorsPage : UserControl |
||||
|
{ |
||||
|
public PipsPagerCustomColorsPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,197 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="ControlCatalog.Pages.PipsPagerCustomTemplatesPage"> |
||||
|
<DockPanel> |
||||
|
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
||||
|
<StackPanel Margin="12" Spacing="8"> |
||||
|
<TextBlock Text="Custom Templates" FontSize="16" FontWeight="SemiBold" |
||||
|
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
||||
|
Text="Override pip item templates using Style selectors targeting the inner ListBoxItem to create squares, pills, numbers, or any custom shape." /> |
||||
|
<Separator /> |
||||
|
<TextBlock Text="Technique" FontSize="13" FontWeight="SemiBold" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" |
||||
|
Text="Target: PipsPager /template/ ListBox ListBoxItem" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" |
||||
|
Text="States: :selected, :pointerover, :pressed" /> |
||||
|
</StackPanel> |
||||
|
</ScrollViewer> |
||||
|
|
||||
|
<Border DockPanel.Dock="Right" Width="1" |
||||
|
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
||||
|
|
||||
|
<StackPanel Spacing="32" Margin="24"> |
||||
|
|
||||
|
<!-- Squares --> |
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Squares" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="5"> |
||||
|
<PipsPager.Styles> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem"> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<Grid Background="Transparent"> |
||||
|
<Rectangle Name="Pip" |
||||
|
Width="4" Height="4" |
||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" |
||||
|
Fill="{DynamicResource PipsPagerSelectionIndicatorForeground}"> |
||||
|
<Rectangle.Transitions> |
||||
|
<Transitions> |
||||
|
<DoubleTransition Property="Width" Duration="0:0:0.167" /> |
||||
|
<DoubleTransition Property="Height" Duration="0:0:0.167" /> |
||||
|
<BrushTransition Property="Fill" Duration="0:0:0.167" /> |
||||
|
</Transitions> |
||||
|
</Rectangle.Transitions> |
||||
|
</Rectangle> |
||||
|
</Grid> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pointerover /template/ Rectangle#Pip"> |
||||
|
<Setter Property="Width" Value="6" /> |
||||
|
<Setter Property="Height" Value="6" /> |
||||
|
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundPointerOver}" /> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Rectangle#Pip"> |
||||
|
<Setter Property="Width" Value="6" /> |
||||
|
<Setter Property="Height" Value="6" /> |
||||
|
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundSelected}" /> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Rectangle#Pip"> |
||||
|
<Setter Property="Width" Value="6" /> |
||||
|
<Setter Property="Height" Value="6" /> |
||||
|
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundSelected}" /> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pressed /template/ Rectangle#Pip"> |
||||
|
<Setter Property="Width" Value="4" /> |
||||
|
<Setter Property="Height" Value="4" /> |
||||
|
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundPressed}" /> |
||||
|
</Style> |
||||
|
</PipsPager.Styles> |
||||
|
</PipsPager> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<!-- Pill-shaped --> |
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Pill-shaped Selected" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="5" |
||||
|
IsPreviousButtonVisible="False" |
||||
|
IsNextButtonVisible="False"> |
||||
|
<PipsPager.Styles> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem"> |
||||
|
<Setter Property="Width" Value="24" /> |
||||
|
<Setter Property="Height" Value="24" /> |
||||
|
<Setter Property="Padding" Value="0" /> |
||||
|
<Setter Property="Margin" Value="2,0" /> |
||||
|
<Setter Property="MinWidth" Value="0" /> |
||||
|
<Setter Property="MinHeight" Value="0" /> |
||||
|
<Setter Property="ClipToBounds" Value="False" /> |
||||
|
<Setter Property="VerticalAlignment" Value="Center" /> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<Grid Background="Transparent"> |
||||
|
<Border Name="Pip" |
||||
|
Width="8" Height="8" CornerRadius="4" |
||||
|
HorizontalAlignment="Center" VerticalAlignment="Center" |
||||
|
Background="#C0C0C0"> |
||||
|
<Border.Transitions> |
||||
|
<Transitions> |
||||
|
<DoubleTransition Property="Width" Duration="0:0:0.2" Easing="CubicEaseOut" /> |
||||
|
<DoubleTransition Property="Height" Duration="0:0:0.2" Easing="CubicEaseOut" /> |
||||
|
<CornerRadiusTransition Property="CornerRadius" Duration="0:0:0.2" Easing="CubicEaseOut" /> |
||||
|
<BrushTransition Property="Background" Duration="0:0:0.2" /> |
||||
|
</Transitions> |
||||
|
</Border.Transitions> |
||||
|
</Border> |
||||
|
</Grid> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pointerover /template/ Border#Pip"> |
||||
|
<Setter Property="Width" Value="10" /> |
||||
|
<Setter Property="Height" Value="10" /> |
||||
|
<Setter Property="CornerRadius" Value="5" /> |
||||
|
<Setter Property="Background" Value="#909090" /> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Border#Pip"> |
||||
|
<Setter Property="Width" Value="24" /> |
||||
|
<Setter Property="Height" Value="8" /> |
||||
|
<Setter Property="CornerRadius" Value="4" /> |
||||
|
<Setter Property="Background" Value="#FF6B35" /> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Border#Pip"> |
||||
|
<Setter Property="Width" Value="24" /> |
||||
|
<Setter Property="Height" Value="8" /> |
||||
|
<Setter Property="CornerRadius" Value="4" /> |
||||
|
<Setter Property="Background" Value="#E85A2A" /> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pressed /template/ Border#Pip"> |
||||
|
<Setter Property="Width" Value="8" /> |
||||
|
<Setter Property="Height" Value="8" /> |
||||
|
<Setter Property="Background" Value="#707070" /> |
||||
|
</Style> |
||||
|
</PipsPager.Styles> |
||||
|
</PipsPager> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<!-- Numbers --> |
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Numbers" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="10" |
||||
|
MaxVisiblePips="4" |
||||
|
IsNextButtonVisible="True" |
||||
|
IsPreviousButtonVisible="True" |
||||
|
ClipToBounds="False"> |
||||
|
<PipsPager.Styles> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem"> |
||||
|
<Setter Property="Width" Value="44" /> |
||||
|
<Setter Property="Height" Value="44" /> |
||||
|
<Setter Property="Padding" Value="0" /> |
||||
|
<Setter Property="Margin" Value="2" /> |
||||
|
<Setter Property="ClipToBounds" Value="False" /> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<Border Name="PipBorder" |
||||
|
Width="30" Height="30" |
||||
|
VerticalAlignment="Center" HorizontalAlignment="Center" |
||||
|
CornerRadius="10" ClipToBounds="False" |
||||
|
Background="LightGray"> |
||||
|
<Border.Transitions> |
||||
|
<Transitions> |
||||
|
<DoubleTransition Property="Width" Duration="0:0:0.2" Easing="CubicEaseOut" /> |
||||
|
<DoubleTransition Property="Height" Duration="0:0:0.2" Easing="CubicEaseOut" /> |
||||
|
<BrushTransition Property="Background" Duration="0:0:0.2" /> |
||||
|
</Transitions> |
||||
|
</Border.Transitions> |
||||
|
<TextBlock Text="{TemplateBinding Content}" |
||||
|
VerticalAlignment="Center" HorizontalAlignment="Center" |
||||
|
Foreground="Black" FontWeight="SemiBold" /> |
||||
|
</Border> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pointerover /template/ Border#PipBorder"> |
||||
|
<Setter Property="Background" Value="DarkGray" /> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:pressed /template/ Border#PipBorder"> |
||||
|
<Setter Property="Width" Value="28" /> |
||||
|
<Setter Property="Height" Value="28" /> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ Border#PipBorder"> |
||||
|
<Setter Property="Background" Value="#0078D7" /> |
||||
|
<Setter Property="Width" Value="36" /> |
||||
|
<Setter Property="Height" Value="36" /> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected /template/ TextBlock"> |
||||
|
<Setter Property="Foreground" Value="White" /> |
||||
|
</Style> |
||||
|
<Style Selector="PipsPager /template/ ListBox ListBoxItem:selected:pointerover /template/ Border#PipBorder"> |
||||
|
<Setter Property="Background" Value="#106EBE" /> |
||||
|
</Style> |
||||
|
</PipsPager.Styles> |
||||
|
</PipsPager> |
||||
|
</StackPanel> |
||||
|
|
||||
|
</StackPanel> |
||||
|
</DockPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace ControlCatalog.Pages; |
||||
|
|
||||
|
public partial class PipsPagerCustomTemplatesPage : UserControl |
||||
|
{ |
||||
|
public PipsPagerCustomTemplatesPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,35 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="ControlCatalog.Pages.PipsPagerEventsPage"> |
||||
|
<DockPanel> |
||||
|
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
||||
|
<StackPanel Margin="12" Spacing="8"> |
||||
|
<TextBlock Text="Events" FontSize="16" FontWeight="SemiBold" |
||||
|
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
||||
|
Text="Monitor SelectedPageIndex changes to react to user navigation." /> |
||||
|
<Separator /> |
||||
|
<TextBlock Text="Event Log" FontSize="13" FontWeight="SemiBold" /> |
||||
|
<ItemsControl Name="EventLog"> |
||||
|
<ItemsControl.ItemTemplate> |
||||
|
<DataTemplate> |
||||
|
<TextBlock Text="{Binding}" FontSize="11" TextWrapping="Wrap" /> |
||||
|
</DataTemplate> |
||||
|
</ItemsControl.ItemTemplate> |
||||
|
</ItemsControl> |
||||
|
</StackPanel> |
||||
|
</ScrollViewer> |
||||
|
|
||||
|
<Border DockPanel.Dock="Right" Width="1" |
||||
|
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
||||
|
|
||||
|
<StackPanel Spacing="16" Margin="24"> |
||||
|
<TextBlock Text="Tap a pip and watch the event log" FontSize="14" Opacity="0.6" /> |
||||
|
<PipsPager Name="EventPager" |
||||
|
NumberOfPages="8" |
||||
|
MaxVisiblePips="8" /> |
||||
|
<TextBlock Name="StatusText" FontSize="13" Opacity="0.7" |
||||
|
Text="Selected: 0" /> |
||||
|
</StackPanel> |
||||
|
</DockPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,29 @@ |
|||||
|
using System.Collections.ObjectModel; |
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace ControlCatalog.Pages; |
||||
|
|
||||
|
public partial class PipsPagerEventsPage : UserControl |
||||
|
{ |
||||
|
private readonly ObservableCollection<string> _events = new(); |
||||
|
|
||||
|
public PipsPagerEventsPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
|
||||
|
EventLog.ItemsSource = _events; |
||||
|
|
||||
|
EventPager.PropertyChanged += (_, e) => |
||||
|
{ |
||||
|
if (e.Property != PipsPager.SelectedPageIndexProperty) |
||||
|
return; |
||||
|
|
||||
|
var newIndex = (int)e.NewValue!; |
||||
|
StatusText.Text = $"Selected: {newIndex}"; |
||||
|
_events.Insert(0, $"SelectedPageIndex changed to {newIndex}"); |
||||
|
|
||||
|
if (_events.Count > 20) |
||||
|
_events.RemoveAt(_events.Count - 1); |
||||
|
}; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,46 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="ControlCatalog.Pages.PipsPagerGettingStartedPage"> |
||||
|
<DockPanel> |
||||
|
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
||||
|
<StackPanel Margin="12" Spacing="8"> |
||||
|
<TextBlock Text="Getting Started" FontSize="16" FontWeight="SemiBold" |
||||
|
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
||||
|
Text="PipsPager lets users navigate a paginated collection using configurable dot indicators with optional navigation buttons." /> |
||||
|
<Separator /> |
||||
|
<TextBlock Text="Features" FontSize="13" FontWeight="SemiBold" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Configurable maximum visible pips" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Automatic scrolling for large collections" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Horizontal and Vertical orientation" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Previous/Next navigation buttons" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="Customizable pips and buttons" /> |
||||
|
</StackPanel> |
||||
|
</ScrollViewer> |
||||
|
|
||||
|
<Border DockPanel.Dock="Right" Width="1" |
||||
|
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
||||
|
|
||||
|
<StackPanel Spacing="24" Margin="24"> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Default" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="5" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Without Navigation Buttons" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="10" |
||||
|
IsPreviousButtonVisible="False" |
||||
|
IsNextButtonVisible="False" |
||||
|
MaxVisiblePips="5" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="Vertical" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="5" Orientation="Vertical" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
</StackPanel> |
||||
|
</DockPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace ControlCatalog.Pages; |
||||
|
|
||||
|
public partial class PipsPagerGettingStartedPage : UserControl |
||||
|
{ |
||||
|
public PipsPagerGettingStartedPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,52 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="ControlCatalog.Pages.PipsPagerLargeCollectionPage"> |
||||
|
<DockPanel> |
||||
|
<ScrollViewer DockPanel.Dock="Right" Width="220"> |
||||
|
<StackPanel Margin="12" Spacing="8"> |
||||
|
<TextBlock Text="Large Collections" FontSize="16" FontWeight="SemiBold" |
||||
|
Foreground="{DynamicResource SystemControlHighlightAccentBrush}" /> |
||||
|
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.7" |
||||
|
Text="Use MaxVisiblePips to limit visible indicators when the page count is large. The pips scroll automatically to keep the selected pip visible." /> |
||||
|
<Separator /> |
||||
|
<TextBlock Text="Properties" FontSize="13" FontWeight="SemiBold" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="NumberOfPages: Total page count" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="MaxVisiblePips: Visible indicator limit" /> |
||||
|
<TextBlock FontSize="12" TextWrapping="Wrap" Text="SelectedPageIndex: Current selection" /> |
||||
|
</StackPanel> |
||||
|
</ScrollViewer> |
||||
|
|
||||
|
<Border DockPanel.Dock="Right" Width="1" |
||||
|
Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" /> |
||||
|
|
||||
|
<StackPanel Spacing="24" Margin="24"> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="50 Pages, MaxVisiblePips=7" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager Name="LargePager" |
||||
|
NumberOfPages="50" |
||||
|
MaxVisiblePips="7" |
||||
|
SelectedPageIndex="25" /> |
||||
|
<TextBlock HorizontalAlignment="Left" FontSize="12"> |
||||
|
<Run Text="Selected: " FontWeight="SemiBold" /> |
||||
|
<Run Text="{Binding #LargePager.SelectedPageIndex}" /> |
||||
|
<Run Text=" / " /> |
||||
|
<Run Text="{Binding #LargePager.NumberOfPages}" /> |
||||
|
</TextBlock> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="20 Pages, MaxVisiblePips=5" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="20" |
||||
|
MaxVisiblePips="5" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
<StackPanel Spacing="8"> |
||||
|
<TextBlock Text="100 Pages, MaxVisiblePips=9" FontWeight="SemiBold" FontSize="14" /> |
||||
|
<PipsPager NumberOfPages="100" |
||||
|
MaxVisiblePips="9" /> |
||||
|
</StackPanel> |
||||
|
|
||||
|
</StackPanel> |
||||
|
</DockPanel> |
||||
|
</UserControl> |
||||
@ -0,0 +1,11 @@ |
|||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace ControlCatalog.Pages; |
||||
|
|
||||
|
public partial class PipsPagerLargeCollectionPage : UserControl |
||||
|
{ |
||||
|
public PipsPagerLargeCollectionPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,11 @@ |
|||||
|
<UserControl xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
||||
|
x:Class="ControlCatalog.Pages.PipsPagerPage"> |
||||
|
<NavigationPage x:Name="SampleNav"> |
||||
|
<NavigationPage.Styles> |
||||
|
<Style Selector="NavigationPage#SampleNav /template/ Border#PART_NavigationBar"> |
||||
|
<Setter Property="Background" Value="Transparent" /> |
||||
|
</Style> |
||||
|
</NavigationPage.Styles> |
||||
|
</NavigationPage> |
||||
|
</UserControl> |
||||
@ -0,0 +1,47 @@ |
|||||
|
using System; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Interactivity; |
||||
|
|
||||
|
namespace ControlCatalog.Pages |
||||
|
{ |
||||
|
public partial class PipsPagerPage : UserControl |
||||
|
{ |
||||
|
private static readonly (string Group, string Title, string Description, Func<UserControl> Factory)[] Demos = |
||||
|
{ |
||||
|
("Getting Started", "First Look", |
||||
|
"Default PipsPager with horizontal and vertical orientation, with and without navigation buttons.", |
||||
|
() => new PipsPagerGettingStartedPage()), |
||||
|
|
||||
|
("Features", "Carousel Integration", |
||||
|
"Bind SelectedPageIndex to a Carousel's SelectedIndex for two-way synchronized page navigation.", |
||||
|
() => new PipsPagerCarouselPage()), |
||||
|
("Features", "Large Collections", |
||||
|
"Use MaxVisiblePips to limit visible indicators when the page count is large. Pips scroll automatically.", |
||||
|
() => new PipsPagerLargeCollectionPage()), |
||||
|
("Features", "Events", |
||||
|
"Monitor SelectedPageIndex changes to react to user navigation.", |
||||
|
() => new PipsPagerEventsPage()), |
||||
|
|
||||
|
("Appearance", "Custom Colors", |
||||
|
"Override pip indicator colors using resource keys for normal, selected, and hover states.", |
||||
|
() => new PipsPagerCustomColorsPage()), |
||||
|
("Appearance", "Custom Buttons", |
||||
|
"Replace the default chevron navigation buttons with custom styled buttons.", |
||||
|
() => new PipsPagerCustomButtonsPage()), |
||||
|
("Appearance", "Custom Templates", |
||||
|
"Override pip item templates to create squares, pills, numbers, or any custom shape.", |
||||
|
() => new PipsPagerCustomTemplatesPage()), |
||||
|
}; |
||||
|
|
||||
|
public PipsPagerPage() |
||||
|
{ |
||||
|
InitializeComponent(); |
||||
|
Loaded += OnLoaded; |
||||
|
} |
||||
|
|
||||
|
private async void OnLoaded(object? sender, RoutedEventArgs e) |
||||
|
{ |
||||
|
await SampleNav.PushAsync(NavigationDemoHelper.CreateGalleryHomePage(SampleNav, Demos), null); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,85 @@ |
|||||
|
using System.Collections.Generic; |
||||
|
using Avalonia.Automation.Provider; |
||||
|
using Avalonia.Controls; |
||||
|
|
||||
|
namespace Avalonia.Automation.Peers |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// An automation peer for <see cref="PipsPager"/>.
|
||||
|
/// </summary>
|
||||
|
public class PipsPagerAutomationPeer : ControlAutomationPeer, ISelectionProvider |
||||
|
{ |
||||
|
private ListBox? _pipsList; |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of the <see cref="PipsPagerAutomationPeer"/> class.
|
||||
|
/// </summary>
|
||||
|
/// <param name="owner">The control associated with this peer.</param>
|
||||
|
public PipsPagerAutomationPeer(PipsPager owner) : base(owner) |
||||
|
{ |
||||
|
owner.SelectedIndexChanged += OnSelectionChanged; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the owner as a <see cref="PipsPager"/>.
|
||||
|
/// </summary>
|
||||
|
private new PipsPager Owner => (PipsPager)base.Owner; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public bool CanSelectMultiple => false; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public bool IsSelectionRequired => true; |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
public IReadOnlyList<AutomationPeer> GetSelection() |
||||
|
{ |
||||
|
var result = new List<AutomationPeer>(); |
||||
|
var owner = Owner; |
||||
|
|
||||
|
if (owner.SelectedPageIndex >= 0 && owner.SelectedPageIndex < owner.NumberOfPages) |
||||
|
{ |
||||
|
_pipsList ??= owner.FindNameScope()?.Find<ListBox>("PART_PipsPagerList"); |
||||
|
|
||||
|
if (_pipsList != null) |
||||
|
{ |
||||
|
var container = _pipsList.ContainerFromIndex(owner.SelectedPageIndex); |
||||
|
if (container is Control c) |
||||
|
{ |
||||
|
var peer = GetOrCreate(c); |
||||
|
result.Add(peer); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override AutomationControlType GetAutomationControlTypeCore() |
||||
|
{ |
||||
|
return AutomationControlType.List; |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override string GetClassNameCore() |
||||
|
{ |
||||
|
return nameof(PipsPager); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override string? GetNameCore() |
||||
|
{ |
||||
|
var name = base.GetNameCore(); |
||||
|
return string.IsNullOrWhiteSpace(name) ? "Pips Pager" : name; |
||||
|
} |
||||
|
|
||||
|
private void OnSelectionChanged(object? sender, Controls.PipsPagerSelectedIndexChangedEventArgs e) |
||||
|
{ |
||||
|
RaisePropertyChangedEvent( |
||||
|
SelectionPatternIdentifiers.SelectionProperty, |
||||
|
e.OldIndex, |
||||
|
e.NewIndex); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,662 @@ |
|||||
|
using System; |
||||
|
using System.Threading; |
||||
|
using Avalonia.Threading; |
||||
|
using Avalonia.Controls.Metadata; |
||||
|
using Avalonia.Automation; |
||||
|
using Avalonia.Automation.Peers; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Input; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Layout; |
||||
|
using Avalonia.Styling; |
||||
|
using System.Collections.Generic; |
||||
|
|
||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Represents a control that lets the user navigate through a paginated collection using a set of pips.
|
||||
|
/// </summary>
|
||||
|
[TemplatePart(PART_PreviousButton, typeof(Button))] |
||||
|
[TemplatePart(PART_NextButton, typeof(Button))] |
||||
|
[TemplatePart(PART_PipsPagerList, typeof(ListBox))] |
||||
|
[PseudoClasses(PC_FirstPage, PC_LastPage, PC_Vertical, PC_Horizontal)] |
||||
|
public class PipsPager : TemplatedControl |
||||
|
{ |
||||
|
private const string PART_PreviousButton = "PART_PreviousButton"; |
||||
|
private const string PART_NextButton = "PART_NextButton"; |
||||
|
private const string PART_PipsPagerList = "PART_PipsPagerList"; |
||||
|
|
||||
|
private const string PC_FirstPage = ":first-page"; |
||||
|
private const string PC_LastPage = ":last-page"; |
||||
|
private const string PC_Vertical = ":vertical"; |
||||
|
private const string PC_Horizontal = ":horizontal"; |
||||
|
|
||||
|
private Button? _previousButton; |
||||
|
private Button? _nextButton; |
||||
|
private ListBox? _pipsPagerList; |
||||
|
private bool _scrollPending; |
||||
|
private bool _updatingPagerSize; |
||||
|
private bool _isInitialLoad; |
||||
|
private int _lastSelectedPageIndex; |
||||
|
private CancellationTokenSource? _scrollAnimationCts; |
||||
|
private PipsPagerTemplateSettings _templateSettings = new PipsPagerTemplateSettings(); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="MaxVisiblePips"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<int> MaxVisiblePipsProperty = |
||||
|
AvaloniaProperty.Register<PipsPager, int>(nameof(MaxVisiblePips), 5); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="IsNextButtonVisible"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<bool> IsNextButtonVisibleProperty = |
||||
|
AvaloniaProperty.Register<PipsPager, bool>(nameof(IsNextButtonVisible), true); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="NumberOfPages"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<int> NumberOfPagesProperty = |
||||
|
AvaloniaProperty.Register<PipsPager, int>(nameof(NumberOfPages)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Orientation"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<Orientation> OrientationProperty = |
||||
|
AvaloniaProperty.Register<PipsPager, Orientation>(nameof(Orientation), Orientation.Horizontal); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="IsPreviousButtonVisible"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<bool> IsPreviousButtonVisibleProperty = |
||||
|
AvaloniaProperty.Register<PipsPager, bool>(nameof(IsPreviousButtonVisible), true); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="SelectedPageIndex"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<int> SelectedPageIndexProperty = |
||||
|
AvaloniaProperty.Register<PipsPager, int>(nameof(SelectedPageIndex), |
||||
|
defaultBindingMode: BindingMode.TwoWay); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="TemplateSettings"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly DirectProperty<PipsPager, PipsPagerTemplateSettings> TemplateSettingsProperty = |
||||
|
AvaloniaProperty.RegisterDirect<PipsPager, PipsPagerTemplateSettings>(nameof(TemplateSettings), |
||||
|
x => x.TemplateSettings); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="PreviousButtonStyle"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<ControlTheme?> PreviousButtonStyleProperty = |
||||
|
AvaloniaProperty.Register<PipsPager, ControlTheme?>(nameof(PreviousButtonStyle)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="NextButtonStyle"/> property.
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<ControlTheme?> NextButtonStyleProperty = |
||||
|
AvaloniaProperty.Register<PipsPager, ControlTheme?>(nameof(NextButtonStyle)); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="SelectedIndexChanged"/> event.
|
||||
|
/// </summary>
|
||||
|
public static readonly RoutedEvent<PipsPagerSelectedIndexChangedEventArgs> SelectedIndexChangedEvent = |
||||
|
RoutedEvent.Register<PipsPager, PipsPagerSelectedIndexChangedEventArgs>(nameof(SelectedIndexChanged), RoutingStrategies.Bubble); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Occurs when the selected index has changed.
|
||||
|
/// </summary>
|
||||
|
public event EventHandler<PipsPagerSelectedIndexChangedEventArgs>? SelectedIndexChanged |
||||
|
{ |
||||
|
add => AddHandler(SelectedIndexChangedEvent, value); |
||||
|
remove => RemoveHandler(SelectedIndexChangedEvent, value); |
||||
|
} |
||||
|
|
||||
|
static PipsPager() |
||||
|
{ |
||||
|
SelectedPageIndexProperty.Changed.AddClassHandler<PipsPager>((x, e) => x.OnSelectedPageIndexChanged(e)); |
||||
|
NumberOfPagesProperty.Changed.AddClassHandler<PipsPager>((x, e) => x.OnNumberOfPagesChanged(e)); |
||||
|
IsPreviousButtonVisibleProperty.Changed.AddClassHandler<PipsPager>((x, e) => x.OnIsPreviousButtonVisibleChanged(e)); |
||||
|
IsNextButtonVisibleProperty.Changed.AddClassHandler<PipsPager>((x, e) => x.OnIsNextButtonVisibleChanged(e)); |
||||
|
OrientationProperty.Changed.AddClassHandler<PipsPager>((x, e) => x.OnOrientationChanged(e)); |
||||
|
MaxVisiblePipsProperty.Changed.AddClassHandler<PipsPager>((x, e) => x.OnMaxVisiblePipsChanged(e)); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Initializes a new instance of <see cref="PipsPager"/>.
|
||||
|
/// </summary>
|
||||
|
public PipsPager() |
||||
|
{ |
||||
|
UpdatePseudoClasses(); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the maximum number of visible pips.
|
||||
|
/// </summary>
|
||||
|
public int MaxVisiblePips |
||||
|
{ |
||||
|
get => GetValue(MaxVisiblePipsProperty); |
||||
|
set => SetValue(MaxVisiblePipsProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the visibility of the next button.
|
||||
|
/// </summary>
|
||||
|
public bool IsNextButtonVisible |
||||
|
{ |
||||
|
get => GetValue(IsNextButtonVisibleProperty); |
||||
|
set => SetValue(IsNextButtonVisibleProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the number of pages.
|
||||
|
/// </summary>
|
||||
|
public int NumberOfPages |
||||
|
{ |
||||
|
get => GetValue(NumberOfPagesProperty); |
||||
|
set => SetValue(NumberOfPagesProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the orientation of the pips.
|
||||
|
/// </summary>
|
||||
|
public Orientation Orientation |
||||
|
{ |
||||
|
get => GetValue(OrientationProperty); |
||||
|
set => SetValue(OrientationProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the visibility of the previous button.
|
||||
|
/// </summary>
|
||||
|
public bool IsPreviousButtonVisible |
||||
|
{ |
||||
|
get => GetValue(IsPreviousButtonVisibleProperty); |
||||
|
set => SetValue(IsPreviousButtonVisibleProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the current selected page index.
|
||||
|
/// </summary>
|
||||
|
public int SelectedPageIndex |
||||
|
{ |
||||
|
get => GetValue(SelectedPageIndexProperty); |
||||
|
set => SetValue(SelectedPageIndexProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the template settings.
|
||||
|
/// </summary>
|
||||
|
public PipsPagerTemplateSettings TemplateSettings |
||||
|
{ |
||||
|
get => _templateSettings; |
||||
|
private set => SetAndRaise(TemplateSettingsProperty, ref _templateSettings, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the style for the previous button.
|
||||
|
/// </summary>
|
||||
|
public ControlTheme? PreviousButtonStyle |
||||
|
{ |
||||
|
get => GetValue(PreviousButtonStyleProperty); |
||||
|
set => SetValue(PreviousButtonStyleProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets or sets the style for the next button.
|
||||
|
/// </summary>
|
||||
|
public ControlTheme? NextButtonStyle |
||||
|
{ |
||||
|
get => GetValue(NextButtonStyleProperty); |
||||
|
set => SetValue(NextButtonStyleProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override AutomationPeer OnCreateAutomationPeer() |
||||
|
{ |
||||
|
return new PipsPagerAutomationPeer(this); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) |
||||
|
{ |
||||
|
base.OnApplyTemplate(e); |
||||
|
|
||||
|
_scrollAnimationCts?.Cancel(); |
||||
|
_scrollAnimationCts?.Dispose(); |
||||
|
_scrollAnimationCts = null; |
||||
|
_isInitialLoad = true; |
||||
|
|
||||
|
// Unsubscribe from previous button events
|
||||
|
if (_previousButton != null) |
||||
|
{ |
||||
|
_previousButton.Click -= PreviousButton_Click; |
||||
|
} |
||||
|
|
||||
|
if (_nextButton != null) |
||||
|
{ |
||||
|
_nextButton.Click -= NextButton_Click; |
||||
|
} |
||||
|
|
||||
|
// Unsubscribe from previous list events
|
||||
|
if (_pipsPagerList != null) |
||||
|
{ |
||||
|
_pipsPagerList.SizeChanged -= OnPipsPagerListSizeChanged; |
||||
|
_pipsPagerList.ContainerPrepared -= OnContainerPrepared; |
||||
|
_pipsPagerList.ContainerIndexChanged -= OnContainerIndexChanged; |
||||
|
} |
||||
|
|
||||
|
// Get template parts
|
||||
|
_previousButton = e.NameScope.Find<Button>(PART_PreviousButton); |
||||
|
_nextButton = e.NameScope.Find<Button>(PART_NextButton); |
||||
|
_pipsPagerList = e.NameScope.Find<ListBox>(PART_PipsPagerList); |
||||
|
|
||||
|
// Set up previous button
|
||||
|
if (_previousButton != null) |
||||
|
{ |
||||
|
_previousButton.Click += PreviousButton_Click; |
||||
|
AutomationProperties.SetName(_previousButton, "Previous page"); |
||||
|
} |
||||
|
|
||||
|
// Set up next button
|
||||
|
if (_nextButton != null) |
||||
|
{ |
||||
|
_nextButton.Click += NextButton_Click; |
||||
|
AutomationProperties.SetName(_nextButton, "Next page"); |
||||
|
} |
||||
|
|
||||
|
// Set up pips list
|
||||
|
if (_pipsPagerList != null) |
||||
|
{ |
||||
|
_pipsPagerList.SizeChanged += OnPipsPagerListSizeChanged; |
||||
|
_pipsPagerList.ContainerPrepared += OnContainerPrepared; |
||||
|
_pipsPagerList.ContainerIndexChanged += OnContainerIndexChanged; |
||||
|
} |
||||
|
|
||||
|
UpdateButtonsState(); |
||||
|
UpdatePseudoClasses(); |
||||
|
UpdatePagerSize(); |
||||
|
} |
||||
|
|
||||
|
/// <inheritdoc/>
|
||||
|
protected override void OnKeyDown(KeyEventArgs e) |
||||
|
{ |
||||
|
base.OnKeyDown(e); |
||||
|
|
||||
|
if (e.Handled) |
||||
|
return; |
||||
|
|
||||
|
var isHorizontal = Orientation == Orientation.Horizontal; |
||||
|
|
||||
|
switch (e.Key) |
||||
|
{ |
||||
|
case Key.Left when isHorizontal: |
||||
|
case Key.Up when !isHorizontal: |
||||
|
if (SelectedPageIndex > 0) |
||||
|
{ |
||||
|
SetCurrentValue(SelectedPageIndexProperty, SelectedPageIndex - 1); |
||||
|
e.Handled = true; |
||||
|
} |
||||
|
break; |
||||
|
case Key.Right when isHorizontal: |
||||
|
case Key.Down when !isHorizontal: |
||||
|
if (SelectedPageIndex < NumberOfPages - 1) |
||||
|
{ |
||||
|
SetCurrentValue(SelectedPageIndexProperty, SelectedPageIndex + 1); |
||||
|
e.Handled = true; |
||||
|
} |
||||
|
break; |
||||
|
case Key.Home: |
||||
|
SetCurrentValue(SelectedPageIndexProperty, 0); |
||||
|
e.Handled = true; |
||||
|
break; |
||||
|
case Key.End: |
||||
|
if (NumberOfPages > 0) |
||||
|
{ |
||||
|
SetCurrentValue(SelectedPageIndexProperty, NumberOfPages - 1); |
||||
|
e.Handled = true; |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void OnSelectedPageIndexChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
var newIndex = e.GetNewValue<int>(); |
||||
|
var oldIndex = e.GetOldValue<int>(); |
||||
|
|
||||
|
if (newIndex < 0) |
||||
|
{ |
||||
|
SetCurrentValue(SelectedPageIndexProperty, 0); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
if (NumberOfPages > 0) |
||||
|
{ |
||||
|
if (newIndex >= NumberOfPages) |
||||
|
{ |
||||
|
SetCurrentValue(SelectedPageIndexProperty, NumberOfPages - 1); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
if (newIndex > 0) |
||||
|
{ |
||||
|
SetCurrentValue(SelectedPageIndexProperty, 0); |
||||
|
return; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_lastSelectedPageIndex = oldIndex; |
||||
|
|
||||
|
UpdateButtonsState(); |
||||
|
UpdatePseudoClasses(); |
||||
|
RequestScrollToSelectedPip(); |
||||
|
|
||||
|
RaiseEvent(new PipsPagerSelectedIndexChangedEventArgs(oldIndex, newIndex)); |
||||
|
} |
||||
|
|
||||
|
private void OnNumberOfPagesChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
var newValue = e.GetNewValue<int>(); |
||||
|
|
||||
|
if (newValue < 0) |
||||
|
{ |
||||
|
SetCurrentValue(NumberOfPagesProperty, 0); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
var pips = TemplateSettings.Pips; |
||||
|
|
||||
|
if (pips.Count < newValue) |
||||
|
{ |
||||
|
var start = pips.Count + 1; |
||||
|
var count = newValue - pips.Count; |
||||
|
var toAdd = new List<int>(count); |
||||
|
for (int i = 0; i < count; i++) |
||||
|
{ |
||||
|
toAdd.Add(start + i); |
||||
|
} |
||||
|
pips.AddRange(toAdd); |
||||
|
} |
||||
|
else if (pips.Count > newValue) |
||||
|
{ |
||||
|
pips.RemoveRange(newValue, pips.Count - newValue); |
||||
|
} |
||||
|
|
||||
|
var indexClamped = false; |
||||
|
|
||||
|
if (newValue > 0 && SelectedPageIndex >= newValue) |
||||
|
{ |
||||
|
SetCurrentValue(SelectedPageIndexProperty, newValue - 1); |
||||
|
indexClamped = true; |
||||
|
} |
||||
|
else if (newValue == 0 && SelectedPageIndex > 0) |
||||
|
{ |
||||
|
SetCurrentValue(SelectedPageIndexProperty, 0); |
||||
|
indexClamped = true; |
||||
|
} |
||||
|
|
||||
|
if (!indexClamped) |
||||
|
{ |
||||
|
UpdateButtonsState(); |
||||
|
UpdatePseudoClasses(); |
||||
|
} |
||||
|
|
||||
|
UpdatePagerSize(); |
||||
|
} |
||||
|
|
||||
|
private void OnIsPreviousButtonVisibleChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
UpdateButtonsState(); |
||||
|
} |
||||
|
|
||||
|
private void OnIsNextButtonVisibleChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
UpdateButtonsState(); |
||||
|
} |
||||
|
|
||||
|
private void OnOrientationChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
UpdatePseudoClasses(); |
||||
|
UpdatePagerSize(); |
||||
|
} |
||||
|
|
||||
|
private void OnMaxVisiblePipsChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
var newValue = e.GetNewValue<int>(); |
||||
|
|
||||
|
if (newValue < 1) |
||||
|
{ |
||||
|
SetCurrentValue(MaxVisiblePipsProperty, 1); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
UpdatePagerSize(); |
||||
|
} |
||||
|
|
||||
|
private void PreviousButton_Click(object? sender, RoutedEventArgs e) |
||||
|
{ |
||||
|
if (SelectedPageIndex > 0) |
||||
|
{ |
||||
|
SetCurrentValue(SelectedPageIndexProperty, SelectedPageIndex - 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void NextButton_Click(object? sender, RoutedEventArgs e) |
||||
|
{ |
||||
|
if (SelectedPageIndex < NumberOfPages - 1) |
||||
|
{ |
||||
|
SetCurrentValue(SelectedPageIndexProperty, SelectedPageIndex + 1); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void UpdateButtonsState() |
||||
|
{ |
||||
|
if (_previousButton != null) |
||||
|
_previousButton.IsEnabled = SelectedPageIndex > 0; |
||||
|
|
||||
|
if (_nextButton != null) |
||||
|
_nextButton.IsEnabled = SelectedPageIndex < NumberOfPages - 1; |
||||
|
} |
||||
|
|
||||
|
private void UpdatePseudoClasses() |
||||
|
{ |
||||
|
PseudoClasses.Set(PC_FirstPage, SelectedPageIndex == 0); |
||||
|
PseudoClasses.Set(PC_LastPage, NumberOfPages > 0 && SelectedPageIndex >= NumberOfPages - 1); |
||||
|
PseudoClasses.Set(PC_Vertical, Orientation == Orientation.Vertical); |
||||
|
PseudoClasses.Set(PC_Horizontal, Orientation == Orientation.Horizontal); |
||||
|
} |
||||
|
|
||||
|
private void OnPipsPagerListSizeChanged(object? sender, SizeChangedEventArgs e) |
||||
|
{ |
||||
|
if (!_updatingPagerSize) |
||||
|
UpdatePagerSize(); |
||||
|
} |
||||
|
|
||||
|
private void OnContainerPrepared(object? sender, ContainerPreparedEventArgs e) |
||||
|
{ |
||||
|
UpdateContainerAutomationProperties(e.Container, e.Index); |
||||
|
} |
||||
|
|
||||
|
private void OnContainerIndexChanged(object? sender, ContainerIndexChangedEventArgs e) |
||||
|
{ |
||||
|
UpdateContainerAutomationProperties(e.Container, e.NewIndex); |
||||
|
} |
||||
|
|
||||
|
private void UpdateContainerAutomationProperties(Control container, int index) |
||||
|
{ |
||||
|
AutomationProperties.SetName(container, $"Page {index + 1}"); |
||||
|
AutomationProperties.SetPositionInSet(container, index + 1); |
||||
|
AutomationProperties.SetSizeOfSet(container, NumberOfPages); |
||||
|
} |
||||
|
|
||||
|
private void RequestScrollToSelectedPip() |
||||
|
{ |
||||
|
if (_scrollPending) |
||||
|
return; |
||||
|
|
||||
|
_scrollPending = true; |
||||
|
Dispatcher.UIThread.Post(() => |
||||
|
{ |
||||
|
_scrollPending = false; |
||||
|
ScrollToSelectedPip(); |
||||
|
}, DispatcherPriority.Input); |
||||
|
} |
||||
|
|
||||
|
private void ScrollToSelectedPip() |
||||
|
{ |
||||
|
if (_pipsPagerList == null) |
||||
|
return; |
||||
|
|
||||
|
if (NumberOfPages <= MaxVisiblePips) |
||||
|
return; |
||||
|
|
||||
|
var scrollViewer = _pipsPagerList.Scroll as ScrollViewer; |
||||
|
if (scrollViewer == null) |
||||
|
return; |
||||
|
|
||||
|
var container = _pipsPagerList.ContainerFromIndex(SelectedPageIndex) as Layoutable; |
||||
|
if (container == null) |
||||
|
return; |
||||
|
|
||||
|
var isHorizontal = Orientation == Orientation.Horizontal; |
||||
|
var pipSize = isHorizontal |
||||
|
? container.Bounds.Width + container.Margin.Left + container.Margin.Right |
||||
|
: container.Bounds.Height + container.Margin.Top + container.Margin.Bottom; |
||||
|
|
||||
|
if (pipSize <= 0) |
||||
|
return; |
||||
|
|
||||
|
var maxVisiblePips = MaxVisiblePips; |
||||
|
var evenOffset = maxVisiblePips % 2 == 0 && SelectedPageIndex > _lastSelectedPageIndex ? 1 : 0; |
||||
|
var offsetElements = SelectedPageIndex + evenOffset - maxVisiblePips / 2; |
||||
|
var targetOffset = Math.Max(0.0, offsetElements * pipSize); |
||||
|
var maxOffset = isHorizontal |
||||
|
? scrollViewer.Extent.Width - scrollViewer.Viewport.Width |
||||
|
: scrollViewer.Extent.Height - scrollViewer.Viewport.Height; |
||||
|
targetOffset = Math.Min(targetOffset, Math.Max(0, maxOffset)); |
||||
|
|
||||
|
if (_isInitialLoad) |
||||
|
{ |
||||
|
_isInitialLoad = false; |
||||
|
scrollViewer.Offset = isHorizontal |
||||
|
? new Vector(targetOffset, scrollViewer.Offset.Y) |
||||
|
: new Vector(scrollViewer.Offset.X, targetOffset); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
AnimateScrollOffset(scrollViewer, targetOffset, isHorizontal); |
||||
|
} |
||||
|
|
||||
|
private void AnimateScrollOffset(ScrollViewer scrollViewer, double targetOffset, bool isHorizontal) |
||||
|
{ |
||||
|
_scrollAnimationCts?.Cancel(); |
||||
|
_scrollAnimationCts?.Dispose(); |
||||
|
_scrollAnimationCts = new CancellationTokenSource(); |
||||
|
var token = _scrollAnimationCts.Token; |
||||
|
|
||||
|
var startOffset = isHorizontal ? scrollViewer.Offset.X : scrollViewer.Offset.Y; |
||||
|
var delta = targetOffset - startOffset; |
||||
|
|
||||
|
if (Math.Abs(delta) < 0.5) |
||||
|
{ |
||||
|
scrollViewer.Offset = isHorizontal |
||||
|
? new Vector(targetOffset, scrollViewer.Offset.Y) |
||||
|
: new Vector(scrollViewer.Offset.X, targetOffset); |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
const int durationMs = 200; |
||||
|
const int frameMs = 16; |
||||
|
var startTime = Environment.TickCount64; |
||||
|
|
||||
|
DispatcherTimer.RunOnce(() => AnimateStep(scrollViewer, startOffset, delta, startTime, durationMs, isHorizontal, token), |
||||
|
TimeSpan.FromMilliseconds(frameMs), DispatcherPriority.Render); |
||||
|
} |
||||
|
|
||||
|
private void AnimateStep(ScrollViewer scrollViewer, double startOffset, double delta, |
||||
|
long startTime, int durationMs, bool isHorizontal, CancellationToken token) |
||||
|
{ |
||||
|
if (token.IsCancellationRequested) |
||||
|
return; |
||||
|
|
||||
|
var elapsed = Environment.TickCount64 - startTime; |
||||
|
var t = Math.Min(1.0, (double)elapsed / durationMs); |
||||
|
var eased = 1.0 - Math.Pow(1.0 - t, 3); |
||||
|
var current = startOffset + (delta * eased); |
||||
|
|
||||
|
scrollViewer.Offset = isHorizontal |
||||
|
? new Vector(current, scrollViewer.Offset.Y) |
||||
|
: new Vector(scrollViewer.Offset.X, current); |
||||
|
|
||||
|
if (t < 1.0) |
||||
|
{ |
||||
|
DispatcherTimer.RunOnce(() => AnimateStep(scrollViewer, startOffset, delta, startTime, durationMs, isHorizontal, token), |
||||
|
TimeSpan.FromMilliseconds(16), DispatcherPriority.Render); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void UpdatePagerSize() |
||||
|
{ |
||||
|
if (_pipsPagerList == null) |
||||
|
return; |
||||
|
|
||||
|
_updatingPagerSize = true; |
||||
|
|
||||
|
try |
||||
|
{ |
||||
|
double pipSize = 12.0; |
||||
|
|
||||
|
var container = _pipsPagerList.ContainerFromIndex(SelectedPageIndex) as Layoutable; |
||||
|
|
||||
|
if (container == null && _pipsPagerList.Items.Count > 0) |
||||
|
container = _pipsPagerList.ContainerFromIndex(0); |
||||
|
|
||||
|
if (container != null) |
||||
|
{ |
||||
|
var margin = container.Margin; |
||||
|
var size = Orientation == Orientation.Horizontal |
||||
|
? container.Bounds.Width + margin.Left + margin.Right |
||||
|
: container.Bounds.Height + margin.Top + margin.Bottom; |
||||
|
|
||||
|
if (size > 0) |
||||
|
pipSize = size; |
||||
|
} |
||||
|
|
||||
|
double spacing = 0.0; |
||||
|
|
||||
|
if (_pipsPagerList.ItemsPanelRoot is StackPanel itemsPanel) |
||||
|
{ |
||||
|
spacing = itemsPanel.Spacing; |
||||
|
} |
||||
|
|
||||
|
var visibleCount = Math.Min(NumberOfPages, MaxVisiblePips); |
||||
|
|
||||
|
if (visibleCount <= 0) |
||||
|
return; |
||||
|
|
||||
|
var extent = (visibleCount * pipSize) + ((visibleCount - 1) * spacing); |
||||
|
|
||||
|
if (Orientation == Orientation.Horizontal) |
||||
|
{ |
||||
|
_pipsPagerList.Width = extent; |
||||
|
_pipsPagerList.Height = double.NaN; |
||||
|
} |
||||
|
else |
||||
|
{ |
||||
|
_pipsPagerList.Height = extent; |
||||
|
_pipsPagerList.Width = double.NaN; |
||||
|
} |
||||
|
|
||||
|
RequestScrollToSelectedPip(); |
||||
|
} |
||||
|
finally |
||||
|
{ |
||||
|
_updatingPagerSize = false; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,27 @@ |
|||||
|
using Avalonia.Interactivity; |
||||
|
|
||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Provides data for the <see cref="PipsPager.SelectedIndexChanged"/> event.
|
||||
|
/// </summary>
|
||||
|
public class PipsPagerSelectedIndexChangedEventArgs : RoutedEventArgs |
||||
|
{ |
||||
|
public PipsPagerSelectedIndexChangedEventArgs(int oldIndex, int newIndex) |
||||
|
: base(PipsPager.SelectedIndexChangedEvent) |
||||
|
{ |
||||
|
OldIndex = oldIndex; |
||||
|
NewIndex = newIndex; |
||||
|
} |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the previous selected index.
|
||||
|
/// </summary>
|
||||
|
public int OldIndex { get; } |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the new selected index.
|
||||
|
/// </summary>
|
||||
|
public int NewIndex { get; } |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,30 @@ |
|||||
|
using Avalonia.Collections; |
||||
|
|
||||
|
namespace Avalonia.Controls.Primitives |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Provides calculated values for use with the <see cref="PipsPager"/>'s control theme or template.
|
||||
|
/// </summary>
|
||||
|
public class PipsPagerTemplateSettings : AvaloniaObject |
||||
|
{ |
||||
|
private AvaloniaList<int> _pips; |
||||
|
|
||||
|
internal PipsPagerTemplateSettings() |
||||
|
{ |
||||
|
_pips = new AvaloniaList<int>(); |
||||
|
} |
||||
|
|
||||
|
public static readonly DirectProperty<PipsPagerTemplateSettings, AvaloniaList<int>> PipsProperty = |
||||
|
AvaloniaProperty.RegisterDirect<PipsPagerTemplateSettings, AvaloniaList<int>>( |
||||
|
nameof(Pips), |
||||
|
o => o.Pips); |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Gets the collection of pips indices.
|
||||
|
/// </summary>
|
||||
|
public AvaloniaList<int> Pips |
||||
|
{ |
||||
|
get => _pips; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,312 @@ |
|||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<Design.PreviewWith> |
||||
|
<PipsPager NumberOfPages="5" SelectedPageIndex="2" /> |
||||
|
</Design.PreviewWith> |
||||
|
|
||||
|
<!-- Icon path data for navigation buttons --> |
||||
|
<!-- Horizontal (left/right arrows) --> |
||||
|
<StreamGeometry x:Key="PipsPagerPreviousPageButtonData">M 8.12,2.29 L 3.41,7.00 C 3.14,7.27 3.14,7.71 3.41,7.98 L 8.12,12.69 C 8.57,13.14 9.33,12.82 9.33,12.19 L 9.33,2.79 C 9.33,2.16 8.57,1.84 8.12,2.29 Z</StreamGeometry> |
||||
|
<StreamGeometry x:Key="PipsPagerNextPageButtonData">M 3.88,2.29 L 8.59,7.00 C 8.86,7.27 8.86,7.71 8.59,7.98 L 3.88,12.69 C 3.43,13.14 2.67,12.82 2.67,12.19 L 2.67,2.79 C 2.67,2.16 3.43,1.84 3.88,2.29 Z</StreamGeometry> |
||||
|
|
||||
|
<!-- Vertical (up/down arrows) --> |
||||
|
<StreamGeometry x:Key="PipsPagerPreviousPageButtonVerticalData">M 2.29,8.12 L 7.00,3.41 C 7.27,3.14 7.71,3.14 7.98,3.41 L 12.69,8.12 C 13.14,8.57 12.82,9.33 12.19,9.33 L 2.79,9.33 C 2.16,9.33 1.84,8.57 2.29,8.12 Z</StreamGeometry> |
||||
|
<StreamGeometry x:Key="PipsPagerNextPageButtonVerticalData">M 2.29,3.88 L 7.00,8.59 C 7.27,8.86 7.71,8.86 7.98,8.59 L 12.69,3.88 C 13.14,3.43 12.82,2.67 12.19,2.67 L 2.79,2.67 C 2.16,2.67 1.84,3.43 2.29,3.88 Z</StreamGeometry> |
||||
|
|
||||
|
<!-- Base navigation button theme (for custom button styles to inherit from) --> |
||||
|
<ControlTheme x:Key="PipsPagerNavigationButtonTheme" TargetType="Button"> |
||||
|
<Setter Property="Background" Value="Transparent" /> |
||||
|
<Setter Property="Foreground" Value="{DynamicResource PipsPagerNavigationButtonForeground}" /> |
||||
|
<Setter Property="BorderBrush" Value="Transparent" /> |
||||
|
<Setter Property="BorderThickness" Value="0" /> |
||||
|
<Setter Property="Width" Value="24" /> |
||||
|
<Setter Property="Height" Value="24" /> |
||||
|
<Setter Property="Padding" Value="0" /> |
||||
|
<Setter Property="HorizontalContentAlignment" Value="Center" /> |
||||
|
<Setter Property="VerticalContentAlignment" Value="Center" /> |
||||
|
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" /> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<ContentPresenter Name="PART_ContentPresenter" |
||||
|
Background="{TemplateBinding Background}" |
||||
|
BorderBrush="{TemplateBinding BorderBrush}" |
||||
|
BorderThickness="{TemplateBinding BorderThickness}" |
||||
|
CornerRadius="{TemplateBinding CornerRadius}" |
||||
|
Content="{TemplateBinding Content}" |
||||
|
ContentTemplate="{TemplateBinding ContentTemplate}" |
||||
|
Padding="{TemplateBinding Padding}" |
||||
|
HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" |
||||
|
VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
|
||||
|
<Style Selector="^:pointerover /template/ ContentPresenter#PART_ContentPresenter"> |
||||
|
<Setter Property="Background" Value="Transparent" /> |
||||
|
<Setter Property="Foreground" Value="{DynamicResource PipsPagerNavigationButtonForegroundPointerOver}" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^:pressed /template/ ContentPresenter#PART_ContentPresenter"> |
||||
|
<Setter Property="Background" Value="Transparent" /> |
||||
|
<Setter Property="Foreground" Value="{DynamicResource PipsPagerNavigationButtonForegroundPressed}" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^:disabled /template/ ContentPresenter#PART_ContentPresenter"> |
||||
|
<Setter Property="Foreground" Value="{DynamicResource PipsPagerNavigationButtonForegroundDisabled}" /> |
||||
|
</Style> |
||||
|
</ControlTheme> |
||||
|
|
||||
|
<!-- Previous button theme --> |
||||
|
<ControlTheme x:Key="PipsPagerPreviousButtonTheme" TargetType="Button" BasedOn="{StaticResource PipsPagerNavigationButtonTheme}" /> |
||||
|
|
||||
|
<!-- Next button theme --> |
||||
|
<ControlTheme x:Key="PipsPagerNextButtonTheme" TargetType="Button" BasedOn="{StaticResource PipsPagerNavigationButtonTheme}" /> |
||||
|
|
||||
|
<ControlTheme x:Key="PipsPagerItemTheme" TargetType="ListBoxItem"> |
||||
|
<Setter Property="Width" Value="12" /> |
||||
|
<Setter Property="Height" Value="24" /> |
||||
|
<Setter Property="Padding" Value="0" /> |
||||
|
<Setter Property="Margin" Value="0" /> |
||||
|
<Setter Property="MinWidth" Value="0" /> |
||||
|
<Setter Property="MinHeight" Value="0" /> |
||||
|
<Setter Property="ClipToBounds" Value="False" /> |
||||
|
<Setter Property="VerticalAlignment" Value="Center" /> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<Grid Background="Transparent"> |
||||
|
<Ellipse Name="Pip" |
||||
|
Width="4" |
||||
|
Height="4" |
||||
|
HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
Fill="{DynamicResource PipsPagerSelectionIndicatorForeground}"> |
||||
|
<Ellipse.Transitions> |
||||
|
<Transitions> |
||||
|
<DoubleTransition Property="Width" Duration="0:0:0.167" /> |
||||
|
<DoubleTransition Property="Height" Duration="0:0:0.167" /> |
||||
|
<BrushTransition Property="Fill" Duration="0:0:0.167" /> |
||||
|
</Transitions> |
||||
|
</Ellipse.Transitions> |
||||
|
</Ellipse> |
||||
|
</Grid> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
|
||||
|
<Style Selector="^:pointerover /template/ Ellipse#Pip"> |
||||
|
<Setter Property="Width" Value="6" /> |
||||
|
<Setter Property="Height" Value="6" /> |
||||
|
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundPointerOver}" /> |
||||
|
</Style> |
||||
|
<Style Selector="^:pressed /template/ Ellipse#Pip"> |
||||
|
<Setter Property="Width" Value="4" /> |
||||
|
<Setter Property="Height" Value="4" /> |
||||
|
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundPressed}" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^:selected /template/ Ellipse#Pip"> |
||||
|
<Setter Property="Width" Value="6" /> |
||||
|
<Setter Property="Height" Value="6" /> |
||||
|
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundSelected}" /> |
||||
|
</Style> |
||||
|
<Style Selector="^:selected:pointerover /template/ Ellipse#Pip"> |
||||
|
<Setter Property="Width" Value="6" /> |
||||
|
<Setter Property="Height" Value="6" /> |
||||
|
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundPointerOver}" /> |
||||
|
</Style> |
||||
|
<Style Selector="^:disabled /template/ Ellipse#Pip"> |
||||
|
<Setter Property="Fill" Value="{DynamicResource PipsPagerSelectionIndicatorForegroundDisabled}" /> |
||||
|
</Style> |
||||
|
</ControlTheme> |
||||
|
|
||||
|
<ControlTheme x:Key="{x:Type PipsPager}" TargetType="PipsPager"> |
||||
|
<Setter Property="Background" Value="Transparent" /> |
||||
|
<Setter Property="HorizontalAlignment" Value="Left" /> |
||||
|
<Setter Property="VerticalAlignment" Value="Top" /> |
||||
|
<Setter Property="IsTabStop" Value="False" /> |
||||
|
<Setter Property="PreviousButtonStyle" Value="{StaticResource PipsPagerPreviousButtonTheme}" /> |
||||
|
<Setter Property="NextButtonStyle" Value="{StaticResource PipsPagerNextButtonTheme}" /> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<StackPanel Name="PART_RootPanel" |
||||
|
Orientation="{TemplateBinding Orientation}" |
||||
|
Background="{TemplateBinding Background}" |
||||
|
ClipToBounds="False"> |
||||
|
<Button Name="PART_PreviousButton" |
||||
|
Theme="{TemplateBinding PreviousButtonStyle}" |
||||
|
IsVisible="{TemplateBinding IsPreviousButtonVisible}" |
||||
|
VerticalAlignment="Center" |
||||
|
HorizontalAlignment="Center"> |
||||
|
<PathIcon Name="PreviousButtonIcon" Width="10" Height="10" |
||||
|
Data="{StaticResource PipsPagerPreviousPageButtonData}" /> |
||||
|
</Button> |
||||
|
|
||||
|
<ListBox Name="PART_PipsPagerList" |
||||
|
Background="Transparent" |
||||
|
BorderThickness="0" |
||||
|
Padding="0" |
||||
|
HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
ScrollViewer.HorizontalScrollBarVisibility="Hidden" |
||||
|
ScrollViewer.VerticalScrollBarVisibility="Hidden" |
||||
|
ItemsSource="{Binding TemplateSettings.Pips, RelativeSource={RelativeSource TemplatedParent}}" |
||||
|
SelectedIndex="{Binding SelectedPageIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" |
||||
|
AutoScrollToSelectedItem="False" |
||||
|
ClipToBounds="False" |
||||
|
ItemContainerTheme="{StaticResource PipsPagerItemTheme}"> |
||||
|
<ListBox.Styles> |
||||
|
<Style Selector="ScrollViewer"> |
||||
|
<Setter Property="ClipToBounds" Value="False" /> |
||||
|
</Style> |
||||
|
</ListBox.Styles> |
||||
|
<ListBox.ItemsPanel> |
||||
|
<ItemsPanelTemplate> |
||||
|
<StackPanel Orientation="{Binding Orientation, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=PipsPager}}" |
||||
|
HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
Spacing="0" /> |
||||
|
</ItemsPanelTemplate> |
||||
|
</ListBox.ItemsPanel> |
||||
|
</ListBox> |
||||
|
|
||||
|
<Button Name="PART_NextButton" |
||||
|
Theme="{TemplateBinding NextButtonStyle}" |
||||
|
IsVisible="{TemplateBinding IsNextButtonVisible}" |
||||
|
VerticalAlignment="Center" |
||||
|
HorizontalAlignment="Center"> |
||||
|
<PathIcon Name="NextButtonIcon" Width="10" Height="10" |
||||
|
Data="{StaticResource PipsPagerNextPageButtonData}" /> |
||||
|
</Button> |
||||
|
</StackPanel> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
|
||||
|
<Style Selector="^:horizontal /template/ StackPanel#PART_RootPanel"> |
||||
|
<Setter Property="Orientation" Value="Horizontal" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^:vertical /template/ ListBox ListBoxItem"> |
||||
|
<Setter Property="Width" Value="24" /> |
||||
|
<Setter Property="Height" Value="12" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^:vertical /template/ StackPanel#PART_RootPanel"> |
||||
|
<Setter Property="Orientation" Value="Vertical" /> |
||||
|
<Setter Property="HorizontalAlignment" Value="Center" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^:vertical /template/ Button#PART_PreviousButton PathIcon#PreviousButtonIcon"> |
||||
|
<Setter Property="Data" Value="{StaticResource PipsPagerPreviousPageButtonVerticalData}" /> |
||||
|
</Style> |
||||
|
<Style Selector="^:vertical /template/ Button#PART_NextButton PathIcon#NextButtonIcon"> |
||||
|
<Setter Property="Data" Value="{StaticResource PipsPagerNextPageButtonVerticalData}" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^:first-page /template/ Button#PART_PreviousButton"> |
||||
|
<Setter Property="Opacity" Value="0" /> |
||||
|
<Setter Property="IsHitTestVisible" Value="False" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^:last-page /template/ Button#PART_NextButton"> |
||||
|
<Setter Property="Opacity" Value="0" /> |
||||
|
<Setter Property="IsHitTestVisible" Value="False" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^ /template/ Button#PART_PreviousButton:pressed"> |
||||
|
<Setter Property="RenderTransformOrigin" Value="0.5, 0.5" /> |
||||
|
<Setter Property="RenderTransform" Value="scale(0.875)" /> |
||||
|
</Style> |
||||
|
<Style Selector="^ /template/ Button#PART_NextButton:pressed"> |
||||
|
<Setter Property="RenderTransformOrigin" Value="0.5, 0.5" /> |
||||
|
<Setter Property="RenderTransform" Value="scale(0.875)" /> |
||||
|
</Style> |
||||
|
</ControlTheme> |
||||
|
|
||||
|
<ResourceDictionary.ThemeDictionaries> |
||||
|
<ResourceDictionary x:Key="Light"> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackground" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundSelected" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrush" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushSelected" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="#72000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="#9E000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPressed" Color="#9E000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="#FF000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundDisabled" Color="#51000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackground" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackgroundPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackgroundPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackgroundDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrush" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrushPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrushPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrushDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForeground" Color="#72000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForegroundPointerOver" Color="#9E000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForegroundPressed" Color="#9E000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForegroundDisabled" Color="#51000000" /> |
||||
|
</ResourceDictionary> |
||||
|
<ResourceDictionary x:Key="Default"> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackground" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundSelected" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrush" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushSelected" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="#72000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="#9E000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPressed" Color="#9E000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="#FF000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundDisabled" Color="#51000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackground" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackgroundPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackgroundPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackgroundDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrush" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrushPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrushPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrushDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForeground" Color="#72000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForegroundPointerOver" Color="#9E000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForegroundPressed" Color="#9E000000" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForegroundDisabled" Color="#51000000" /> |
||||
|
</ResourceDictionary> |
||||
|
<ResourceDictionary x:Key="Dark"> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackground" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundSelected" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBackgroundDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrush" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushSelected" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorBorderBrushDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForeground" Color="#8BFFFFFF" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPointerOver" Color="#C5FFFFFF" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundPressed" Color="#C5FFFFFF" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundSelected" Color="#FFFFFFFF" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerSelectionIndicatorForegroundDisabled" Color="#3FFFFFFF" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackground" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackgroundPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackgroundPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBackgroundDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrush" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrushPointerOver" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrushPressed" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonBorderBrushDisabled" Color="Transparent" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForeground" Color="#8BFFFFFF" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForegroundPointerOver" Color="#C5FFFFFF" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForegroundPressed" Color="#C5FFFFFF" /> |
||||
|
<SolidColorBrush x:Key="PipsPagerNavigationButtonForegroundDisabled" Color="#3FFFFFFF" /> |
||||
|
</ResourceDictionary> |
||||
|
</ResourceDictionary.ThemeDictionaries> |
||||
|
</ResourceDictionary> |
||||
@ -0,0 +1,143 @@ |
|||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui" |
||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<Design.PreviewWith> |
||||
|
<PipsPager NumberOfPages="5" SelectedPageIndex="2" /> |
||||
|
</Design.PreviewWith> |
||||
|
|
||||
|
<ControlTheme x:Key="{x:Type PipsPager}" TargetType="PipsPager"> |
||||
|
<Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" /> |
||||
|
<Setter Property="Background" Value="Transparent" /> |
||||
|
<Setter Property="BorderBrush" Value="Transparent" /> |
||||
|
<Setter Property="BorderThickness" Value="0" /> |
||||
|
<Setter Property="IsTabStop" Value="False" /> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<StackPanel Name="PART_RootPanel" |
||||
|
Orientation="{TemplateBinding Orientation}" |
||||
|
HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
ClipToBounds="False"> |
||||
|
|
||||
|
<Button Name="PART_PreviousButton" |
||||
|
Width="24" |
||||
|
Height="24" |
||||
|
Padding="0" |
||||
|
HorizontalContentAlignment="Center" |
||||
|
VerticalContentAlignment="Center" |
||||
|
IsVisible="{TemplateBinding IsPreviousButtonVisible}" |
||||
|
VerticalAlignment="Center" |
||||
|
HorizontalAlignment="Center" |
||||
|
Margin="4"> |
||||
|
<PathIcon Width="12" Height="12" Data="M 8.12,2.29 L 3.41,7.00 C 3.14,7.27 3.14,7.71 3.41,7.98 L 8.12,12.69 C 8.57,13.14 9.33,12.82 9.33,12.19 L 9.33,2.79 C 9.33,2.16 8.57,1.84 8.12,2.29 Z" /> |
||||
|
</Button> |
||||
|
|
||||
|
<ListBox Name="PART_PipsPagerList" |
||||
|
Background="Transparent" |
||||
|
BorderThickness="0" |
||||
|
Padding="0" |
||||
|
HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
ScrollViewer.HorizontalScrollBarVisibility="Hidden" |
||||
|
ScrollViewer.VerticalScrollBarVisibility="Hidden" |
||||
|
ItemsSource="{Binding TemplateSettings.Pips, RelativeSource={RelativeSource TemplatedParent}}" |
||||
|
SelectedIndex="{Binding SelectedPageIndex, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" |
||||
|
AutoScrollToSelectedItem="False" |
||||
|
ClipToBounds="False"> |
||||
|
<ListBox.ItemsPanel> |
||||
|
<ItemsPanelTemplate> |
||||
|
<StackPanel Orientation="{Binding Orientation, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=PipsPager}}" |
||||
|
HorizontalAlignment="Center" |
||||
|
VerticalAlignment="Center" |
||||
|
Spacing="4" /> |
||||
|
</ItemsPanelTemplate> |
||||
|
</ListBox.ItemsPanel> |
||||
|
<ListBox.Styles> |
||||
|
<Style Selector="ScrollViewer"> |
||||
|
<Setter Property="ClipToBounds" Value="False" /> |
||||
|
</Style> |
||||
|
<Style Selector="ListBoxItem"> |
||||
|
<Setter Property="Width" Value="12" /> |
||||
|
<Setter Property="Height" Value="24" /> |
||||
|
<Setter Property="Padding" Value="0" /> |
||||
|
<Setter Property="Margin" Value="0" /> |
||||
|
<Setter Property="MinHeight" Value="0" /> |
||||
|
<Setter Property="MinWidth" Value="0" /> |
||||
|
<Setter Property="ClipToBounds" Value="False" /> |
||||
|
<Setter Property="VerticalAlignment" Value="Center" /> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<Panel Background="Transparent"> |
||||
|
<Ellipse Name="Pip" Width="12" Height="12" Fill="{DynamicResource ThemeControlLowBrush}"> |
||||
|
<Ellipse.Transitions> |
||||
|
<Transitions> |
||||
|
<BrushTransition Property="Fill" Duration="0:0:0.1" /> |
||||
|
</Transitions> |
||||
|
</Ellipse.Transitions> |
||||
|
</Ellipse> |
||||
|
</Panel> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
<Style Selector="ListBoxItem:pointerover /template/ Ellipse#Pip"> |
||||
|
<Setter Property="Fill" Value="{DynamicResource ThemeControlHighBrush}" /> |
||||
|
</Style> |
||||
|
<Style Selector="ListBoxItem:selected /template/ Ellipse#Pip"> |
||||
|
<Setter Property="Fill" Value="{DynamicResource ThemeAccentBrush}" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="PipsPager:vertical ListBoxItem"> |
||||
|
<Setter Property="Width" Value="24" /> |
||||
|
<Setter Property="Height" Value="12" /> |
||||
|
</Style> |
||||
|
<Style Selector="ListBoxItem:selected:pointerover /template/ Ellipse#Pip"> |
||||
|
<Setter Property="Fill" Value="{DynamicResource ThemeAccentBrush2}" /> |
||||
|
</Style> |
||||
|
<Style Selector="ListBoxItem:pressed /template/ Ellipse#Pip"> |
||||
|
<Setter Property="Fill" Value="{DynamicResource ThemeAccentBrush3}" /> |
||||
|
</Style> |
||||
|
</ListBox.Styles> |
||||
|
</ListBox> |
||||
|
|
||||
|
<Button Name="PART_NextButton" |
||||
|
Width="24" |
||||
|
Height="24" |
||||
|
Padding="0" |
||||
|
HorizontalContentAlignment="Center" |
||||
|
VerticalContentAlignment="Center" |
||||
|
IsVisible="{TemplateBinding IsNextButtonVisible}" |
||||
|
VerticalAlignment="Center" |
||||
|
HorizontalAlignment="Center" |
||||
|
Margin="4"> |
||||
|
<PathIcon Width="12" Height="12" Data="M 3.88,2.29 L 8.59,7.00 C 8.86,7.27 8.86,7.71 8.59,7.98 L 3.88,12.69 C 3.43,13.14 2.67,12.82 2.67,12.19 L 2.67,2.79 C 2.67,2.16 3.43,1.84 3.88,2.29 Z" /> |
||||
|
</Button> |
||||
|
</StackPanel> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
|
||||
|
<Style Selector="^:vertical /template/ Button#PART_PreviousButton PathIcon"> |
||||
|
<Setter Property="Data" Value="M 2.29,9.33 L 7.00,4.62 C 7.27,4.35 7.71,4.35 7.98,4.62 L 12.69,9.33 C 13.14,9.78 12.82,10.54 12.19,10.54 L 2.79,10.54 C 2.16,10.54 1.84,9.78 2.29,9.33 Z" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^:vertical /template/ Button#PART_NextButton PathIcon"> |
||||
|
<Setter Property="Data" Value="M 2.29,4.46 L 7.00,9.17 C 7.27,9.44 7.71,9.44 7.98,9.17 L 12.69,4.46 C 13.14,4.01 12.82,3.25 12.19,3.25 L 2.79,3.25 C 2.16,3.25 1.84,4.01 2.29,4.46 Z" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^ /template/ Button"> |
||||
|
<Setter Property="Transitions"> |
||||
|
<Transitions> |
||||
|
<BrushTransition Property="Background" Duration="0:0:0.1" /> |
||||
|
</Transitions> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^:first-page /template/ Button#PART_PreviousButton"> |
||||
|
<Setter Property="Opacity" Value="0" /> |
||||
|
<Setter Property="IsHitTestVisible" Value="False" /> |
||||
|
</Style> |
||||
|
|
||||
|
<Style Selector="^:last-page /template/ Button#PART_NextButton"> |
||||
|
<Setter Property="Opacity" Value="0" /> |
||||
|
<Setter Property="IsHitTestVisible" Value="False" /> |
||||
|
</Style> |
||||
|
</ControlTheme> |
||||
|
</ResourceDictionary> |
||||
@ -0,0 +1,578 @@ |
|||||
|
using Avalonia.Input; |
||||
|
using Avalonia.UnitTests; |
||||
|
using Avalonia.VisualTree; |
||||
|
using Avalonia.Interactivity; |
||||
|
using Avalonia.Layout; |
||||
|
using Avalonia.Controls.Primitives; |
||||
|
using Avalonia.Controls.Templates; |
||||
|
using System.Linq; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Controls.UnitTests |
||||
|
{ |
||||
|
public class PipsPagerTests : ScopedTestBase |
||||
|
{ |
||||
|
[Fact] |
||||
|
public void NumberOfPages_Should_Update_Pips() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
|
||||
|
target.NumberOfPages = 5; |
||||
|
|
||||
|
Assert.Equal(5, target.TemplateSettings.Pips.Count); |
||||
|
Assert.Equal(1, target.TemplateSettings.Pips[0]); |
||||
|
Assert.Equal(5, target.TemplateSettings.Pips[4]); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Decreasing_NumberOfPages_Should_Update_Pips() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
target.NumberOfPages = 5; |
||||
|
|
||||
|
target.NumberOfPages = 3; |
||||
|
|
||||
|
Assert.Equal(3, target.TemplateSettings.Pips.Count); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Decreasing_NumberOfPages_Should_Update_SelectedPageIndex() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
target.NumberOfPages = 5; |
||||
|
target.SelectedPageIndex = 4; |
||||
|
|
||||
|
target.NumberOfPages = 3; |
||||
|
|
||||
|
Assert.Equal(2, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SelectedPageIndex_Should_Be_Clamped_To_Zero() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
target.NumberOfPages = 5; |
||||
|
|
||||
|
target.SelectedPageIndex = -1; |
||||
|
|
||||
|
Assert.Equal(0, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SelectedPageIndex_Change_Should_Raise_Event() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
target.NumberOfPages = 5; |
||||
|
var raised = false; |
||||
|
target.SelectedIndexChanged += (s, e) => raised = true; |
||||
|
|
||||
|
target.SelectedPageIndex = 2; |
||||
|
|
||||
|
Assert.True(raised); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Next_Button_Should_Increment_Index() |
||||
|
{ |
||||
|
using var unittestApplication = UnitTestApplication.Start(TestServices.StyledWindow); |
||||
|
|
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 5, |
||||
|
SelectedPageIndex = 1, |
||||
|
IsNextButtonVisible = true, |
||||
|
Template = GetTemplate() |
||||
|
}; |
||||
|
|
||||
|
var root = new TestRoot(target); |
||||
|
target.ApplyTemplate(); |
||||
|
|
||||
|
var nextButton = target.GetVisualDescendants().OfType<Button>().FirstOrDefault(b => b.Name == "PART_NextButton"); |
||||
|
Assert.NotNull(nextButton); |
||||
|
|
||||
|
nextButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); |
||||
|
|
||||
|
Assert.Equal(2, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Previous_Button_Should_Decrement_Index() |
||||
|
{ |
||||
|
using var unittestApplication = UnitTestApplication.Start(TestServices.StyledWindow); |
||||
|
|
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 5, |
||||
|
SelectedPageIndex = 3, |
||||
|
IsPreviousButtonVisible = true, |
||||
|
Template = GetTemplate() |
||||
|
}; |
||||
|
|
||||
|
var root = new TestRoot(target); |
||||
|
target.ApplyTemplate(); |
||||
|
|
||||
|
var prevButton = target.GetVisualDescendants().OfType<Button>().FirstOrDefault(b => b.Name == "PART_PreviousButton"); |
||||
|
Assert.NotNull(prevButton); |
||||
|
|
||||
|
prevButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); |
||||
|
|
||||
|
Assert.Equal(2, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Keyboard_Navigation_Should_Work() |
||||
|
{ |
||||
|
using var unittestApplication = UnitTestApplication.Start(TestServices.StyledWindow); |
||||
|
|
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 5, |
||||
|
SelectedPageIndex = 1, |
||||
|
Orientation = Orientation.Horizontal |
||||
|
}; |
||||
|
|
||||
|
var root = new TestRoot(target); |
||||
|
target.ApplyTemplate(); |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { Key = Key.Right, RoutedEvent = InputElement.KeyDownEvent }); |
||||
|
Assert.Equal(2, target.SelectedPageIndex); |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { Key = Key.Left, RoutedEvent = InputElement.KeyDownEvent }); |
||||
|
Assert.Equal(1, target.SelectedPageIndex); |
||||
|
|
||||
|
target.Orientation = Orientation.Vertical; |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { Key = Key.Down, RoutedEvent = InputElement.KeyDownEvent }); |
||||
|
Assert.Equal(2, target.SelectedPageIndex); |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { Key = Key.Up, RoutedEvent = InputElement.KeyDownEvent }); |
||||
|
Assert.Equal(1, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Orientation_PseudoClasses_Should_Be_Set() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
|
||||
|
target.Orientation = Orientation.Horizontal; |
||||
|
Assert.True(target.Classes.Contains(":horizontal")); |
||||
|
Assert.False(target.Classes.Contains(":vertical")); |
||||
|
|
||||
|
target.Orientation = Orientation.Vertical; |
||||
|
Assert.False(target.Classes.Contains(":horizontal")); |
||||
|
Assert.True(target.Classes.Contains(":vertical")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Clamping_Logic_Works() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
target.NumberOfPages = 5; |
||||
|
|
||||
|
target.SelectedPageIndex = 10; |
||||
|
Assert.Equal(4, target.SelectedPageIndex); |
||||
|
|
||||
|
target.SelectedPageIndex = -5; |
||||
|
Assert.Equal(0, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Manual_Button_Visibility_Should_Be_Respected() |
||||
|
{ |
||||
|
using var unittestApplication = UnitTestApplication.Start(TestServices.StyledWindow); |
||||
|
|
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 5, |
||||
|
IsPreviousButtonVisible = false, |
||||
|
IsNextButtonVisible = false, |
||||
|
Template = GetTemplate() |
||||
|
}; |
||||
|
|
||||
|
var root = new TestRoot(target); |
||||
|
target.ApplyTemplate(); |
||||
|
|
||||
|
Assert.False(target.IsPreviousButtonVisible); |
||||
|
Assert.False(target.IsNextButtonVisible); |
||||
|
|
||||
|
target.IsPreviousButtonVisible = true; |
||||
|
target.IsNextButtonVisible = true; |
||||
|
Assert.True(target.IsPreviousButtonVisible); |
||||
|
Assert.True(target.IsNextButtonVisible); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Rapid_Page_Changes_Should_Maintain_Integrity() |
||||
|
{ |
||||
|
var target = new PipsPager { NumberOfPages = 100 }; |
||||
|
var list = new System.Collections.Generic.List<int>(); |
||||
|
target.SelectedIndexChanged += (s, e) => list.Add(e.NewIndex); |
||||
|
|
||||
|
for (int i = 1; i <= 50; i++) |
||||
|
{ |
||||
|
target.SelectedPageIndex = i; |
||||
|
} |
||||
|
|
||||
|
Assert.Equal(50, list.Count); |
||||
|
Assert.Equal(50, target.SelectedPageIndex); |
||||
|
Assert.Equal(50, list.Last()); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SelectedIndexChanged_Event_Should_Have_Correct_Args() |
||||
|
{ |
||||
|
var target = new PipsPager { NumberOfPages = 5, SelectedPageIndex = 1 }; |
||||
|
int oldIdx = -1; |
||||
|
int newIdx = -1; |
||||
|
target.SelectedIndexChanged += (s, e) => |
||||
|
{ |
||||
|
oldIdx = e.OldIndex; |
||||
|
newIdx = e.NewIndex; |
||||
|
}; |
||||
|
|
||||
|
target.SelectedPageIndex = 3; |
||||
|
Assert.Equal(1, oldIdx); |
||||
|
Assert.Equal(3, newIdx); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Pager_Size_Should_Update_Based_On_Orientation_And_MaxVisiblePips() |
||||
|
{ |
||||
|
using var unittestApplication = UnitTestApplication.Start(TestServices.StyledWindow); |
||||
|
|
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 10, |
||||
|
MaxVisiblePips = 5, |
||||
|
Orientation = Orientation.Horizontal, |
||||
|
Template = GetTemplate() |
||||
|
}; |
||||
|
|
||||
|
var root = new TestRoot(target); |
||||
|
target.ApplyTemplate(); |
||||
|
|
||||
|
var pipsList = target.GetVisualDescendants().OfType<ListBox>().First(i => i.Name == "PART_PipsPagerList"); |
||||
|
|
||||
|
Assert.Equal(60, pipsList.Width); |
||||
|
|
||||
|
target.Orientation = Orientation.Vertical; |
||||
|
Assert.Equal(60, pipsList.Height); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void NumberOfPages_Reduction_Should_Clamp_SelectedPageIndex() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
target.NumberOfPages = 10; |
||||
|
target.SelectedPageIndex = 8; |
||||
|
|
||||
|
target.NumberOfPages = 5; |
||||
|
Assert.Equal(4, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Page_PseudoClasses_Should_Be_Set() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
target.NumberOfPages = 5; |
||||
|
|
||||
|
target.SelectedPageIndex = 0; |
||||
|
Assert.True(target.Classes.Contains(":first-page")); |
||||
|
Assert.False(target.Classes.Contains(":last-page")); |
||||
|
|
||||
|
target.SelectedPageIndex = 2; |
||||
|
Assert.False(target.Classes.Contains(":first-page")); |
||||
|
Assert.False(target.Classes.Contains(":last-page")); |
||||
|
|
||||
|
target.SelectedPageIndex = 4; |
||||
|
Assert.False(target.Classes.Contains(":first-page")); |
||||
|
Assert.True(target.Classes.Contains(":last-page")); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Navigation_Buttons_IsEnabled_Should_Update() |
||||
|
{ |
||||
|
using var unittestApplication = UnitTestApplication.Start(TestServices.StyledWindow); |
||||
|
|
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 3, |
||||
|
Template = GetTemplate() |
||||
|
}; |
||||
|
|
||||
|
var root = new TestRoot(target); |
||||
|
target.ApplyTemplate(); |
||||
|
|
||||
|
var prevButton = target.GetVisualDescendants().OfType<Button>().First(b => b.Name == "PART_PreviousButton"); |
||||
|
var nextButton = target.GetVisualDescendants().OfType<Button>().First(b => b.Name == "PART_NextButton"); |
||||
|
|
||||
|
target.SelectedPageIndex = 0; |
||||
|
Assert.False(prevButton.IsEnabled); |
||||
|
Assert.True(nextButton.IsEnabled); |
||||
|
|
||||
|
target.SelectedPageIndex = 1; |
||||
|
Assert.True(prevButton.IsEnabled); |
||||
|
Assert.True(nextButton.IsEnabled); |
||||
|
|
||||
|
target.SelectedPageIndex = 2; |
||||
|
Assert.True(prevButton.IsEnabled); |
||||
|
Assert.False(nextButton.IsEnabled); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Horizontal_Keyboard_Navigation_Should_Work() |
||||
|
{ |
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 5, |
||||
|
SelectedPageIndex = 1, |
||||
|
Orientation = Orientation.Horizontal |
||||
|
}; |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Right }); |
||||
|
Assert.Equal(2, target.SelectedPageIndex); |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Left }); |
||||
|
Assert.Equal(1, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Vertical_Keyboard_Navigation_Should_Work() |
||||
|
{ |
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 5, |
||||
|
SelectedPageIndex = 1, |
||||
|
Orientation = Orientation.Vertical |
||||
|
}; |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Down }); |
||||
|
Assert.Equal(2, target.SelectedPageIndex); |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Up }); |
||||
|
Assert.Equal(1, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void NumberOfPages_Zero_Should_Clamp_Index() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
target.NumberOfPages = 0; |
||||
|
target.SelectedPageIndex = 5; |
||||
|
|
||||
|
Assert.Equal(0, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Home_Key_Should_Navigate_To_First_Page() |
||||
|
{ |
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 10, |
||||
|
SelectedPageIndex = 7, |
||||
|
Orientation = Orientation.Horizontal |
||||
|
}; |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Home }); |
||||
|
|
||||
|
Assert.Equal(0, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void End_Key_Should_Navigate_To_Last_Page() |
||||
|
{ |
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 10, |
||||
|
SelectedPageIndex = 3, |
||||
|
Orientation = Orientation.Horizontal |
||||
|
}; |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.End }); |
||||
|
|
||||
|
Assert.Equal(9, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Home_End_Keys_Should_Work_In_Vertical_Orientation() |
||||
|
{ |
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 10, |
||||
|
SelectedPageIndex = 5, |
||||
|
Orientation = Orientation.Vertical |
||||
|
}; |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Home }); |
||||
|
Assert.Equal(0, target.SelectedPageIndex); |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.End }); |
||||
|
Assert.Equal(9, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Negative_NumberOfPages_Should_Be_Coerced_To_Zero() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
|
||||
|
target.NumberOfPages = -5; |
||||
|
|
||||
|
Assert.Equal(0, target.NumberOfPages); |
||||
|
Assert.Equal(0, target.TemplateSettings.Pips.Count); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Next_Button_At_Last_Page_Should_Not_Change_Index() |
||||
|
{ |
||||
|
using var unittestApplication = UnitTestApplication.Start(TestServices.StyledWindow); |
||||
|
|
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 3, |
||||
|
SelectedPageIndex = 2, |
||||
|
Template = GetTemplate() |
||||
|
}; |
||||
|
|
||||
|
var root = new TestRoot(target); |
||||
|
target.ApplyTemplate(); |
||||
|
|
||||
|
var nextButton = target.GetVisualDescendants().OfType<Button>().First(b => b.Name == "PART_NextButton"); |
||||
|
nextButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); |
||||
|
|
||||
|
Assert.Equal(2, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Previous_Button_At_First_Page_Should_Not_Change_Index() |
||||
|
{ |
||||
|
using var unittestApplication = UnitTestApplication.Start(TestServices.StyledWindow); |
||||
|
|
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 3, |
||||
|
SelectedPageIndex = 0, |
||||
|
Template = GetTemplate() |
||||
|
}; |
||||
|
|
||||
|
var root = new TestRoot(target); |
||||
|
target.ApplyTemplate(); |
||||
|
|
||||
|
var prevButton = target.GetVisualDescendants().OfType<Button>().First(b => b.Name == "PART_PreviousButton"); |
||||
|
prevButton.RaiseEvent(new RoutedEventArgs(Button.ClickEvent)); |
||||
|
|
||||
|
Assert.Equal(0, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Arrow_Keys_At_Boundaries_Should_Not_Change_Index() |
||||
|
{ |
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 5, |
||||
|
SelectedPageIndex = 0, |
||||
|
Orientation = Orientation.Horizontal |
||||
|
}; |
||||
|
|
||||
|
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Left }); |
||||
|
Assert.Equal(0, target.SelectedPageIndex); |
||||
|
|
||||
|
target.SelectedPageIndex = 4; |
||||
|
target.RaiseEvent(new KeyEventArgs { RoutedEvent = InputElement.KeyDownEvent, Key = Key.Right }); |
||||
|
Assert.Equal(4, target.SelectedPageIndex); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void SelectedPageIndex_Default_Binding_Mode_Should_Be_TwoWay() |
||||
|
{ |
||||
|
Assert.Equal(Data.BindingMode.TwoWay, PipsPager.SelectedPageIndexProperty.GetMetadata(typeof(PipsPager)).DefaultBindingMode); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void TemplateSettings_Should_Not_Be_Externally_Settable() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
|
||||
|
// TemplateSettings property should have a private setter (compile-time enforcement).
|
||||
|
// Verify the property is readable and initialized.
|
||||
|
Assert.NotNull(target.TemplateSettings); |
||||
|
Assert.IsType<PipsPagerTemplateSettings>(target.TemplateSettings); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void NumberOfPages_To_Zero_Should_Clamp_SelectedPageIndex() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
target.NumberOfPages = 5; |
||||
|
target.SelectedPageIndex = 3; |
||||
|
|
||||
|
target.NumberOfPages = 0; |
||||
|
|
||||
|
Assert.Equal(0, target.SelectedPageIndex); |
||||
|
Assert.Equal(0, target.TemplateSettings.Pips.Count); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Negative_NumberOfPages_After_Having_Pages_Should_Coerce() |
||||
|
{ |
||||
|
var target = new PipsPager(); |
||||
|
target.NumberOfPages = 5; |
||||
|
Assert.Equal(5, target.TemplateSettings.Pips.Count); |
||||
|
|
||||
|
target.NumberOfPages = -1; |
||||
|
|
||||
|
Assert.Equal(0, target.NumberOfPages); |
||||
|
Assert.Equal(0, target.TemplateSettings.Pips.Count); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Preselected_Index_Should_Be_Preserved_After_Template_Apply() |
||||
|
{ |
||||
|
using var app = UnitTestApplication.Start(TestServices.StyledWindow); |
||||
|
|
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 20, |
||||
|
MaxVisiblePips = 5, |
||||
|
SelectedPageIndex = 15, |
||||
|
Template = GetTemplate() |
||||
|
}; |
||||
|
|
||||
|
var root = new TestRoot(target); |
||||
|
target.ApplyTemplate(); |
||||
|
|
||||
|
Assert.Equal(15, target.SelectedPageIndex); |
||||
|
Assert.True(target.Classes.Contains(":last-page") == false); |
||||
|
Assert.True(target.Classes.Contains(":first-page") == false); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public void Preselected_Last_Index_Should_Set_LastPage_PseudoClass() |
||||
|
{ |
||||
|
var target = new PipsPager |
||||
|
{ |
||||
|
NumberOfPages = 10, |
||||
|
SelectedPageIndex = 9 |
||||
|
}; |
||||
|
|
||||
|
Assert.Equal(9, target.SelectedPageIndex); |
||||
|
Assert.True(target.Classes.Contains(":last-page")); |
||||
|
Assert.False(target.Classes.Contains(":first-page")); |
||||
|
} |
||||
|
|
||||
|
private static FuncControlTemplate<PipsPager> GetTemplate() |
||||
|
{ |
||||
|
return new FuncControlTemplate<PipsPager>((parent, scope) => |
||||
|
{ |
||||
|
return new StackPanel |
||||
|
{ |
||||
|
Children = |
||||
|
{ |
||||
|
new Button { Name = "PART_PreviousButton" }.RegisterInNameScope(scope), |
||||
|
new ListBox { Name = "PART_PipsPagerList" }.RegisterInNameScope(scope), |
||||
|
new Button { Name = "PART_NextButton" }.RegisterInNameScope(scope) |
||||
|
} |
||||
|
}; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,168 @@ |
|||||
|
using System.Threading.Tasks; |
||||
|
using Avalonia.Controls; |
||||
|
using Avalonia.Controls.Presenters; |
||||
|
using Avalonia.Controls.Templates; |
||||
|
using Avalonia.Data; |
||||
|
using Avalonia.Layout; |
||||
|
using Avalonia.Media; |
||||
|
using Avalonia.Controls.Shapes; |
||||
|
using Avalonia.Styling; |
||||
|
using Avalonia.Threading; |
||||
|
using Xunit; |
||||
|
|
||||
|
namespace Avalonia.Skia.RenderTests |
||||
|
{ |
||||
|
public class PipsPagerTests : TestBase |
||||
|
{ |
||||
|
public PipsPagerTests() |
||||
|
: base(@"Controls/PipsPager") |
||||
|
{ |
||||
|
} |
||||
|
|
||||
|
private static IControlTemplate CreatePipsPagerTemplate() |
||||
|
{ |
||||
|
return new FuncControlTemplate<PipsPager>((control, scope) => |
||||
|
{ |
||||
|
var stackPanel = new StackPanel |
||||
|
{ |
||||
|
Name = "PART_RootPanel", |
||||
|
Spacing = 5, |
||||
|
[!StackPanel.OrientationProperty] = control[!PipsPager.OrientationProperty], |
||||
|
HorizontalAlignment = HorizontalAlignment.Center, |
||||
|
VerticalAlignment = VerticalAlignment.Center |
||||
|
}; |
||||
|
|
||||
|
var buttonTemplate = new FuncControlTemplate<Button>((b, s) => |
||||
|
new Border |
||||
|
{ |
||||
|
Background = Brushes.LightGray, |
||||
|
Child = new TextBlock |
||||
|
{ |
||||
|
[!TextBlock.TextProperty] = b[!Button.ContentProperty], |
||||
|
FontFamily = TestFontFamily, |
||||
|
VerticalAlignment = VerticalAlignment.Center, |
||||
|
HorizontalAlignment = HorizontalAlignment.Center |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
var prevButton = new Button |
||||
|
{ |
||||
|
Name = "PART_PreviousButton", |
||||
|
Content = "<", |
||||
|
Template = buttonTemplate, |
||||
|
Width = 20, |
||||
|
Height = 20, |
||||
|
[!Button.IsVisibleProperty] = control[!PipsPager.IsPreviousButtonVisibleProperty] |
||||
|
}.RegisterInNameScope(scope); |
||||
|
|
||||
|
var nextButton = new Button |
||||
|
{ |
||||
|
Name = "PART_NextButton", |
||||
|
Content = ">", |
||||
|
Template = buttonTemplate, |
||||
|
Width = 20, |
||||
|
Height = 20, |
||||
|
[!Button.IsVisibleProperty] = control[!PipsPager.IsNextButtonVisibleProperty] |
||||
|
}.RegisterInNameScope(scope); |
||||
|
|
||||
|
// Simple ListBox Template (ItemsPresenter)
|
||||
|
var listBoxTemplate = new FuncControlTemplate<ListBox>((lb, s) => |
||||
|
new ItemsPresenter |
||||
|
{ |
||||
|
Name = "PART_ItemsPresenter", |
||||
|
[~ItemsPresenter.ItemsPanelProperty] = lb[~ListBox.ItemsPanelProperty], |
||||
|
}.RegisterInNameScope(s)); |
||||
|
|
||||
|
var pipsList = new ListBox |
||||
|
{ |
||||
|
Name = "PART_PipsPagerList", |
||||
|
Template = listBoxTemplate, |
||||
|
[!ListBox.ItemsSourceProperty] = new Binding("TemplateSettings.Pips") { Source = control }, |
||||
|
[!ListBox.SelectedIndexProperty] = control[!PipsPager.SelectedPageIndexProperty], |
||||
|
ItemsPanel = new FuncTemplate<Panel?>(() => new StackPanel |
||||
|
{ |
||||
|
Spacing = 2, |
||||
|
[!StackPanel.OrientationProperty] = control[!PipsPager.OrientationProperty] |
||||
|
}) |
||||
|
}.RegisterInNameScope(scope); |
||||
|
|
||||
|
// Default Item Style
|
||||
|
var itemStyle = new Style(x => x.OfType<ListBoxItem>()); |
||||
|
itemStyle.Setters.Add(new Setter(ListBoxItem.TemplateProperty, new FuncControlTemplate<ListBoxItem>((item, s) => |
||||
|
new Ellipse { Name="Pip", Width = 10, Height = 10 }.RegisterInNameScope(s)))); |
||||
|
|
||||
|
// Default Pip Fill Style
|
||||
|
var defaultPipStyle = new Style(x => x.OfType<ListBoxItem>().Template().Name("Pip")); |
||||
|
defaultPipStyle.Setters.Add(new Setter(Ellipse.FillProperty, Brushes.Gray)); |
||||
|
|
||||
|
// Selected Item Style
|
||||
|
var selectedStyle = new Style(x => x.OfType<ListBoxItem>().Class(":selected").Template().Name("Pip")); |
||||
|
selectedStyle.Setters.Add(new Setter(Ellipse.FillProperty, Brushes.Red)); |
||||
|
|
||||
|
pipsList.Styles.Add(itemStyle); |
||||
|
pipsList.Styles.Add(defaultPipStyle); |
||||
|
pipsList.Styles.Add(selectedStyle); |
||||
|
|
||||
|
stackPanel.Children.Add(prevButton); |
||||
|
stackPanel.Children.Add(pipsList); |
||||
|
stackPanel.Children.Add(nextButton); |
||||
|
|
||||
|
return stackPanel; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task PipsPager_Default() |
||||
|
{ |
||||
|
var pipsPager = new PipsPager |
||||
|
{ |
||||
|
Template = CreatePipsPagerTemplate(), |
||||
|
NumberOfPages = 5, |
||||
|
SelectedPageIndex = 1 |
||||
|
}; |
||||
|
|
||||
|
var target = new Border |
||||
|
{ |
||||
|
Padding = new Thickness(20), |
||||
|
Background = Brushes.White, |
||||
|
Child = pipsPager, |
||||
|
Width = 400, |
||||
|
Height = 150 |
||||
|
}; |
||||
|
|
||||
|
target.Measure(new Size(400, 150)); |
||||
|
target.Arrange(new Rect(0, 0, 400, 150)); |
||||
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken); |
||||
|
|
||||
|
await RenderToFile(target); |
||||
|
CompareImages(); |
||||
|
} |
||||
|
|
||||
|
[Fact] |
||||
|
public async Task PipsPager_Preselected_Index() |
||||
|
{ |
||||
|
var pipsPager = new PipsPager |
||||
|
{ |
||||
|
Template = CreatePipsPagerTemplate(), |
||||
|
NumberOfPages = 5, |
||||
|
SelectedPageIndex = 3 |
||||
|
}; |
||||
|
|
||||
|
var target = new Border |
||||
|
{ |
||||
|
Padding = new Thickness(20), |
||||
|
Background = Brushes.White, |
||||
|
Child = pipsPager, |
||||
|
Width = 400, |
||||
|
Height = 150 |
||||
|
}; |
||||
|
|
||||
|
target.Measure(new Size(400, 150)); |
||||
|
target.Arrange(new Rect(0, 0, 400, 150)); |
||||
|
Dispatcher.UIThread.RunJobs(null, TestContext.Current.CancellationToken); |
||||
|
|
||||
|
await RenderToFile(target); |
||||
|
CompareImages(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
Loading…
Reference in new issue