Merge pull request #265 from irihitech/calendar

CalendarView and DatePicker and DateRangePicker
This commit is contained in:
Dong Bin
2024-06-28 15:59:15 +08:00
committed by GitHub
29 changed files with 2575 additions and 10 deletions

View File

@@ -0,0 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ursa.Demo.Pages.DatePickerDemo">
<StackPanel Margin="20" HorizontalAlignment="Left">
<u:CalendarView DateSelected="CalendarView_OnOnDateSelected" DatePreviewed="CalendarView_OnOnDatePreviewed"/>
<TextBlock Text="{Binding #singlePicker.SelectedDate}" ></TextBlock>
<u:DatePicker Name="singlePicker" Width="200" />
<u:DateRangePicker Width="300" DisplayFormat="yyyyMMdd" />
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,25 @@
using System.Diagnostics;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Ursa.Controls;
namespace Ursa.Demo.Pages;
public partial class DatePickerDemo : UserControl
{
public DatePickerDemo()
{
InitializeComponent();
}
private void CalendarView_OnOnDateSelected(object? sender, CalendarDayButtonEventArgs e)
{
Debug.WriteLine("Pressed: "+ e.Date?.ToLongDateString());
}
private void CalendarView_OnOnDatePreviewed(object? sender, CalendarDayButtonEventArgs e)
{
Debug.WriteLine("Hovered: "+e.Date?.ToLongDateString());
}
}

View File

@@ -0,0 +1,6 @@
namespace Ursa.Demo.ViewModels;
public class DatePickerDemoViewModel
{
}

View File

@@ -31,6 +31,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyBreadcrumb => new BreadcrumbDemoViewModel(),
MenuKeys.MenuKeyClassInput => new ClassInputDemoViewModel(),
MenuKeys.MenuKeyClock => new ClockDemoViewModel(),
MenuKeys.MenuKeyDatePicker => new DatePickerDemoViewModel(),
MenuKeys.MenuKeyDialog => new DialogDemoViewModel(),
MenuKeys.MenuKeyDivider => new DividerDemoViewModel(),
MenuKeys.MenuKeyDisableContainer => new DisableContainerDemoViewModel(),

View File

@@ -18,6 +18,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Button Group", Key = MenuKeys.MenuKeyButtonGroup },
new() { MenuHeader = "Class Input", Key = MenuKeys.MenuKeyClassInput },
new() { MenuHeader = "Clock", Key = MenuKeys.MenuKeyClock, Status = "New" },
new() { MenuHeader = "Date Picker", Key = MenuKeys.MenuKeyDatePicker },
new() { MenuHeader = "Dialog", Key = MenuKeys.MenuKeyDialog },
new() { MenuHeader = "Disable Container", Key = MenuKeys.MenuKeyDisableContainer },
new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider },
@@ -65,6 +66,7 @@ public static class MenuKeys
public const string MenuKeyBreadcrumb= "Breadcrumb";
public const string MenuKeyClassInput = "Class Input";
public const string MenuKeyClock = "Clock";
public const string MenuKeyDatePicker = "DatePicker";
public const string MenuKeyDialog = "Dialog";
public const string MenuKeyDivider = "Divider";
public const string MenuKeyDisableContainer = "DisableContainer";

View File

@@ -0,0 +1,291 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<Design.PreviewWith>
<ThemeVariantScope RequestedThemeVariant="Dark">
<StackPanel Width="600" Height="400">
<u:DatePicker HorizontalAlignment="Stretch" />
<u:DateRangePicker />
</StackPanel>
</ThemeVariantScope>
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type u:CalendarDayButton}" TargetType="u:CalendarDayButton">
<Setter Property="MinWidth" Value="{DynamicResource CalenderDayMinWidth}" />
<Setter Property="MinHeight" Value="{DynamicResource CalenderDayMinHeight}" />
<Setter Property="Margin" Value="{DynamicResource CalenderDayMargin}" />
<Setter Property="Background" Value="{DynamicResource CalenderDayBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource CalenderDayBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource CalenderDayBorderThickness}" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="CornerRadius" Value="{DynamicResource CalenderDayCornerRadius}" />
<Setter Property="Template">
<ControlTemplate TargetType="u:CalendarDayButton">
<Panel>
<Border
Name="PART_Background"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter
Name="PART_ContentPresenter"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Content}"
Foreground="{TemplateBinding Foreground}" />
</Border>
</Panel>
</ControlTemplate>
</Setter>
<Style Selector="^:in-range">
<Setter Property="Background" Value="{DynamicResource CalenderDayInRangeBackground}" />
<Setter Property="CornerRadius" Value="{DynamicResource CalenderDayInRangeCornerRadius}" />
<Setter Property="BorderThickness" Value="{DynamicResource CalenderDayNoBorderThickness}" />
</Style>
<Style Selector="^:today">
<Setter Property="Background" Value="{DynamicResource CalenderDayTodayBackground}" />
<Setter Property="Foreground" Value="{DynamicResource CalenderDayTodayForeground}" />
<Setter Property="Cursor" Value="Hand" />
<Style Selector="^:in-range">
<Setter Property="Background" Value="{DynamicResource CalenderDayInRangeBackground}" />
</Style>
</Style>
<Style Selector="^:not-current-month">
<Setter Property="Foreground" Value="{DynamicResource CalenderDayNotCurrentMonthForeground}" />
</Style>
<Style Selector="^:pointerover">
<Setter Property="Background" Value="{DynamicResource CalenderDayPointeroverBackground}" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="^:pressed">
<Setter Property="Background" Value="{DynamicResource CalenderDayPressedBackground}" />
</Style>
<Style Selector="^:selected">
<Setter Property="Background" Value="{DynamicResource CalenderDaySelectedBackground}" />
<Setter Property="CornerRadius" Value="{DynamicResource CalenderDayCornerRadius}" />
<Setter Property="BorderThickness" Value="{DynamicResource CalenderDayNoBorderThickness}" />
<Setter Property="Foreground" Value="{DynamicResource CalenderDaySelectedForeground}" />
<Style Selector="^:pointerover">
<Setter Property="Background" Value="{DynamicResource CalenderDaySelectedPointeroverBackground}" />
</Style>
<Style Selector="^:pressed">
<Setter Property="Background" Value="{DynamicResource CalenderDaySelectedPressedBackground}" />
</Style>
</Style>
<Style Selector="^:start-date">
<Setter Property="CornerRadius" Value="{DynamicResource CalenderDayStartDateCornerRadius}" />
<Setter Property="Background" Value="{DynamicResource CalenderDayStartEndDateBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource CalenderDayStartEndDateBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource CalenderDaySelectedForeground}" />
<Style Selector="^:pointerover">
<Setter Property="Background" Value="{DynamicResource CalenderDayStartEndDatePointeroverBackground}" />
</Style>
<Style Selector="^:pressed">
<Setter Property="Background" Value="{DynamicResource CalenderDayStartEndDatePressedBackground}" />
</Style>
<Style Selector="^:in-range">
<Setter Property="BorderBrush" Value="{DynamicResource CalenderDayStartEndDateInRangeBackground}" />
<Setter Property="BorderThickness" Value="{DynamicResource CalenderDayBorderThickness}" />
</Style>
</Style>
<Style Selector="^:end-date">
<Setter Property="CornerRadius" Value="{DynamicResource CalenderDayEndDateCornerRadius}" />
<Setter Property="Background" Value="{DynamicResource CalenderDayStartEndDateBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource CalenderDayStartEndDateBorderBrush}" />
<Setter Property="Foreground" Value="{DynamicResource CalenderDaySelectedForeground}" />
<Style Selector="^:pointerover">
<Setter Property="Background" Value="{DynamicResource CalenderDayStartEndDatePointeroverBackground}" />
</Style>
<Style Selector="^:pressed">
<Setter Property="Background" Value="{DynamicResource CalenderDayStartEndDatePressedBackground}" />
</Style>
<Style Selector="^:in-range">
<Setter Property="BorderBrush" Value="{DynamicResource CalenderDayStartEndDateInRangeBackground}" />
<Setter Property="BorderThickness" Value="{DynamicResource CalenderDayBorderThickness}" />
</Style>
</Style>
<Style Selector="^:preview-start-date">
<Setter Property="CornerRadius" Value="{DynamicResource CalenderDayStartDateCornerRadius}" />
<Setter Property="Background" Value="{DynamicResource CalenderDayPreviewStartEndDateBackground}" />
<Style Selector="^:pressed">
<Setter Property="Background" Value="{DynamicResource CalenderDayPreviewStartEndDatePressedBackground}" />
</Style>
</Style>
<Style Selector="^:preview-end-date">
<Setter Property="CornerRadius" Value="{DynamicResource CalenderDayEndDateCornerRadius}" />
<Setter Property="Background" Value="{DynamicResource CalenderDayPreviewStartEndDateBackground}" />
<Style Selector="^:pressed">
<Setter Property="Background" Value="{DynamicResource CalenderDayPreviewStartEndDatePressedBackground}" />
</Style>
</Style>
<Style Selector="^:blackout">
<Setter Property="Foreground" Value="{DynamicResource CalenderDayBlackoutForeground}" />
<Setter Property="Background" Value="{DynamicResource CalenderDayBlackoutBackground}" />
<Setter Property="Cursor" Value="No" />
<Style Selector="^:today">
<Setter Property="Foreground" Value="{DynamicResource CalenderDayBlackoutTodayBackground}" />
</Style>
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:CalendarYearButton}" TargetType="u:CalendarYearButton">
<Setter Property="MinWidth" Value="{DynamicResource CalenderYearMinWidth}" />
<Setter Property="MinHeight" Value="{DynamicResource CalenderYearMinHeight }" />
<Setter Property="Margin" Value="{DynamicResource CalenderYearMargin}" />
<Setter Property="Background" Value="{DynamicResource CalenderYearBackground}" />
<Setter Property="BorderBrush" Value="{DynamicResource CalenderYearBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource CalenderYearBorderThickness}" />
<Setter Property="CornerRadius" Value="{DynamicResource CalenderYearCornerRadius}" />
<Setter Property="Template">
<ControlTemplate TargetType="u:CalendarDayButton">
<Panel>
<Border
Name="PART_Background"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<ContentPresenter
Name="PART_ContentPresenter"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Content}"
Foreground="{TemplateBinding Foreground}" />
</Border>
</Panel>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover">
<Setter Property="Background" Value="{DynamicResource CalenderYearPointeroverBackground}" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="^:pressed">
<Setter Property="Background" Value="{DynamicResource CalenderYearPressedBackground}" />
</Style>
<Style Selector="^:selected">
<Setter Property="Background" Value="{DynamicResource CalenderYearSelectedBackground}" />
<Setter Property="CornerRadius" Value="{DynamicResource CalenderYearCornerRadius}" />
<Setter Property="BorderThickness" Value="{DynamicResource CalenderYearSelectedBorderThickness}" />
<Setter Property="Foreground" Value="{DynamicResource CalenderYearSelectedForeground}" />
<Style Selector="^:pointerover">
<Setter Property="Background" Value="{DynamicResource CalenderYearSelectedPointeroverBackground}" />
</Style>
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:CalendarView}" TargetType="u:CalendarView">
<Setter Property="MinHeight" Value="{DynamicResource CalenderViewMinWidth}" />
<Setter Property="MinWidth" Value="{DynamicResource CalenderViewMinHeight}" />
<Setter Property="Background" Value="{DynamicResource DatePickerViewBackground}" />
<Setter Property="Template">
<ControlTemplate TargetType="u:CalendarView">
<Grid Background="{TemplateBinding Background}" RowDefinitions="Auto, *">
<Grid
Grid.Row="0"
Margin="0,0,0,16"
ColumnDefinitions="Auto, Auto,*, Auto, Auto">
<Button
Name="{x:Static u:CalendarView.PART_FastPreviousButton}"
Grid.Column="0"
HorizontalContentAlignment="Left"
Foreground="{TemplateBinding Foreground}"
Theme="{DynamicResource BorderlessButton}">
<PathIcon
Width="{DynamicResource CalenderViewPathIconWidth}"
Height="{DynamicResource CalenderViewPathIconHeight}"
Data="{DynamicResource CalendarViewFastForwardGlyph}"
Foreground="{DynamicResource CalendarItemIconForeground}" />
</Button>
<Button
Name="{x:Static u:CalendarView.PART_PreviousButton}"
Grid.Column="1"
HorizontalContentAlignment="Left"
Foreground="{TemplateBinding Foreground}"
Theme="{DynamicResource BorderlessButton}">
<PathIcon
Width="{DynamicResource CalenderViewPathIconWidth}"
Height="{DynamicResource CalenderViewPathIconHeight}"
Data="{DynamicResource CalendarItemPreviousIconGlyph}"
Foreground="{DynamicResource CalendarItemIconForeground}" />
</Button>
<Grid Grid.Column="2" ColumnDefinitions="*, *">
<Button
Name="{x:Static u:CalendarView.PART_YearButton}"
Grid.Column="0"
HorizontalContentAlignment="Center"
Foreground="{TemplateBinding Foreground}"
Theme="{DynamicResource BorderlessButton}" />
<Button
Name="{x:Static u:CalendarView.PART_MonthButton}"
Grid.Column="1"
HorizontalContentAlignment="Center"
Foreground="{TemplateBinding Foreground}"
Theme="{DynamicResource BorderlessButton}" />
<Button
Name="{x:Static u:CalendarView.PART_HeaderButton}"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"
IsVisible="True" />
</Grid>
<Button
Name="{x:Static u:CalendarView.PART_NextButton}"
Grid.Column="3"
HorizontalContentAlignment="Left"
Foreground="{TemplateBinding Foreground}"
Theme="{DynamicResource BorderlessButton}">
<PathIcon
Width="{DynamicResource CalenderViewPathIconWidth}"
Height="{DynamicResource CalenderViewPathIconHeight}"
Data="{DynamicResource CalendarItemNextIconGlyph}"
Foreground="{DynamicResource CalendarItemIconForeground}" />
</Button>
<Button
Name="{x:Static u:CalendarView.PART_FastNextButton}"
Grid.Column="4"
HorizontalContentAlignment="Left"
Foreground="{TemplateBinding Foreground}"
Theme="{DynamicResource BorderlessButton}">
<PathIcon
Width="{DynamicResource CalenderViewPathIconWidth}"
Height="{DynamicResource CalenderViewPathIconHeight}"
Data="{DynamicResource CalendarViewFastBackwardGlyph}"
Foreground="{DynamicResource CalendarItemIconForeground}" />
</Button>
</Grid>
<Grid
Name="{x:Static u:CalendarView.PART_MonthGrid}"
Grid.Row="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ColumnDefinitions="*, *, *, *, *, *, *"
RowDefinitions="*, Auto, *, *, *, *, *, *" />
<Grid
Name="{x:Static u:CalendarView.PART_YearGrid}"
Grid.Row="1"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ColumnDefinitions="*, *, *"
RowDefinitions="*, *, *, *" />
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

View File

@@ -0,0 +1,126 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:u="https://irihi.tech/ursa">
<!-- Add Resources Here -->
<ControlTheme x:Key="{x:Type u:DatePicker}" TargetType="u:DatePicker">
<Setter Property="Background" Value="{DynamicResource CalendarDatePickerBackground}" />
<Setter Property="Foreground" Value="{DynamicResource CalendarDatePickerForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource CalendarDatePickerBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource CalendarDatePickerBorderThickness}" />
<Setter Property="CornerRadius" Value="{DynamicResource CalendarDatePickerCornerRadius}" />
<Setter Property="IsTodayHighlighted" Value="True" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Padding" Value="8 0" />
<Setter Property="MinHeight" Value="{DynamicResource CalendarDatePickerDefaultHeight}" />
<Setter Property="Template">
<ControlTemplate TargetType="u:DatePicker">
<DataValidationErrors>
<Panel
x:Name="LayoutRoot"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border
x:Name="Background"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Grid
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
ColumnDefinitions="*, Auto, Auto">
<TextBox
Name="PART_TextBox"
Grid.Column="0"
Grid.ColumnSpan="2"
MinHeight="{TemplateBinding MinHeight}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
CornerRadius="{TemplateBinding CornerRadius}"
Foreground="{TemplateBinding Foreground}"
InnerLeftContent="{TemplateBinding InnerLeftContent}"
InnerRightContent="{TemplateBinding InnerRightContent}"
Theme="{DynamicResource LooklessTextBox}"
Watermark="{TemplateBinding Watermark}" />
<Button
Name="ClearButton"
Grid.Column="1"
Padding="0,0,8,0"
Command="{Binding $parent[u:DatePicker].Clear}"
Content="{DynamicResource IconButtonClearData}"
Focusable="False"
IsVisible="False"
Theme="{DynamicResource InnerIconButton}" />
<Button
Name="PART_Button"
Grid.Column="2"
Padding="0,0,8,0"
Content="{DynamicResource CalendarDatePickerIconGlyph}"
Focusable="False"
Theme="{DynamicResource InnerIconButton}" />
<Popup
Name="PART_Popup"
Grid.Column="0"
HorizontalOffset="-4"
IsLightDismissEnabled="True"
IsOpen="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=IsDropdownOpen, Mode=TwoWay}"
Placement="BottomEdgeAlignedLeft"
PlacementTarget="{TemplateBinding}">
<Border
Margin="8"
Padding="8"
Background="{DynamicResource ComboBoxPopupBackground}"
BorderBrush="{DynamicResource ComboBoxPopupBorderBrush}"
BoxShadow="{DynamicResource ComboBoxPopupBoxShadow}"
BorderThickness="{DynamicResource ComboBoxPopupBorderThickness}"
CornerRadius="{DynamicResource CalendarCornerRadius}">
<u:CalendarView
Name="PART_Calendar"
BorderThickness="0"
CornerRadius="{Binding $parent[Border].CornerRadius}"
FirstDayOfWeek="{TemplateBinding FirstDayOfWeek}"
IsTodayHighlighted="{TemplateBinding IsTodayHighlighted}" />
</Border>
</Popup>
</Grid>
</Panel>
</DataValidationErrors>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover">
<Style Selector="^ /template/ Border#Background">
<Setter Property="Background" Value="{DynamicResource CalendarDatePickerPointeroverBackground}" />
</Style>
</Style>
<!-- Disabled State -->
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#Background">
<Setter Property="Background" Value="{DynamicResource CalendarDatePickerDisabledBackground}" />
</Style>
<Style Selector="^ /template/ Button#PART_Button">
<Setter Property="Foreground" Value="{DynamicResource CalendarDatePickerDisabledIconForeground}" />
</Style>
<Style Selector="^ /template/ TextBox#PART_TextBox">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
</Style>
<!-- Focused State -->
<Style Selector="^:focus /template/ Border#Background">
<Setter Property="BorderBrush" Value="{DynamicResource CalendarDatePickerFocusBorderBrush}" />
</Style>
<Style Selector="^:focus-within /template/ Border#Background">
<Setter Property="BorderBrush" Value="{DynamicResource CalendarDatePickerFocusBorderBrush}" />
</Style>
</ControlTheme>
</ResourceDictionary>

View File

@@ -0,0 +1,172 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:contracts="https://irihi.tech/shared"
xmlns:u="https://irihi.tech/ursa">
<!-- Add Resources Here -->
<ControlTheme x:Key="{x:Type u:DateRangePicker}" TargetType="u:DateRangePicker">
<Setter Property="Background" Value="{DynamicResource TextBoxDefaultBackground}" />
<Setter Property="Foreground" Value="{DynamicResource TextBoxForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource TextBoxDefaultBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource TextBoxBorderThickness}" />
<Setter Property="CornerRadius" Value="{DynamicResource TextBoxDefaultCornerRadius}" />
<Setter Property="MinHeight" Value="32" />
<Setter Property="Template">
<ControlTemplate TargetType="u:DateRangePicker">
<DataValidationErrors>
<Panel
Name="LayoutRoot"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border
Name="Background"
HorizontalAlignment="Stretch"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Grid ColumnDefinitions="*, Auto, * Auto">
<TextBox
Name="{x:Static u:DateRangePicker.PART_StartTextBox}"
Grid.Column="0"
MinHeight="{TemplateBinding MinHeight}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
BorderThickness="1"
CornerRadius="3 0 0 3"
Foreground="{TemplateBinding Foreground}"
InnerLeftContent="{TemplateBinding InnerLeftContent}"
IsReadOnly="{TemplateBinding IsReadonly}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="~" />
<TextBox
Name="{x:Static u:DateRangePicker.PART_EndTextBox}"
Grid.Column="2"
MinHeight="{TemplateBinding MinHeight}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
CornerRadius="0"
Foreground="{TemplateBinding Foreground}"
InnerRightContent="{TemplateBinding InnerRightContent}"
IsReadOnly="{TemplateBinding IsReadonly}" />
<Button
Name="ClearButton"
Grid.Column="3"
Padding="8,0"
Command="{Binding $parent[contracts:IClearControl].Clear}"
Content="{DynamicResource IconButtonClearData}"
Focusable="False"
IsVisible="False"
Theme="{DynamicResource InnerIconButton}" />
<Button
Name="{x:Static u:DateRangePicker.PART_Button}"
Grid.Column="3"
Padding="8,0"
Content="{DynamicResource CalendarDatePickerIconGlyph}"
Focusable="False"
Theme="{DynamicResource InnerIconButton}" />
</Grid>
<Popup
Name="{x:Static contracts:PartNames.PART_Popup}"
HorizontalOffset="-4"
IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsDropdownOpen,
Mode=TwoWay}"
Placement="BottomEdgeAlignedLeft"
PlacementTarget="Background">
<Border
Margin="8"
Padding="8"
HorizontalAlignment="Stretch"
Background="{DynamicResource ComboBoxPopupBackground}"
BorderBrush="{DynamicResource ComboBoxPopupBorderBrush}"
BorderThickness="{DynamicResource ComboBoxPopupBorderThickness}"
BoxShadow="{DynamicResource ComboBoxPopupBoxShadow}"
CornerRadius="{DynamicResource CalendarCornerRadius}">
<DockPanel>
<!--
<StackPanel DockPanel.Dock="Bottom" IsVisible="{TemplateBinding NeedConfirmation}">
<Button
Margin="8"
HorizontalAlignment="Right"
Command="{Binding $parent[u:TimeRangePicker].Confirm}"
Content="{DynamicResource STRING_DATE_TIME_CONFIRM}" />
</StackPanel>
-->
<ContentPresenter
Name="PART_PopupHeader"
Margin="8,8,8,0"
Content="{TemplateBinding PopupInnerTopContent}"
DockPanel.Dock="Top"
IsVisible="{TemplateBinding PopupInnerTopContent,
Converter={x:Static ObjectConverters.IsNotNull}}" />
<ContentPresenter
Name="PART_PopupFooter"
Margin="8,0,8,8"
Content="{TemplateBinding PopupInnerBottomContent}"
DockPanel.Dock="Bottom"
IsVisible="{TemplateBinding PopupInnerBottomContent,
Converter={x:Static ObjectConverters.IsNotNull}}" />
<Grid ColumnDefinitions="*, *">
<u:CalendarView
Name="{x:Static u:DateRangePicker.PART_StartCalendar}"
Grid.Column="0"
Margin="8"
BorderThickness="0"
CornerRadius="{Binding $parent[Border].CornerRadius}"
FirstDayOfWeek="{TemplateBinding FirstDayOfWeek}"
IsTodayHighlighted="{TemplateBinding IsTodayHighlighted}" />
<u:CalendarView
Name="{x:Static u:DateRangePicker.PART_EndCalendar}"
Grid.Column="1"
Margin="8"
BorderThickness="0"
CornerRadius="{Binding $parent[Border].CornerRadius}"
FirstDayOfWeek="{TemplateBinding FirstDayOfWeek}"
IsTodayHighlighted="{TemplateBinding IsTodayHighlighted}" />
</Grid>
</DockPanel>
</Border>
</Popup>
</Panel>
</DataValidationErrors>
</ControlTemplate>
</Setter>
<Style Selector="^.clearButton, ^.ClearButton">
<Style Selector="^:pointerover /template/ Button#ClearButton">
<Setter Property="IsVisible" Value="{Binding $parent[u:TimePicker].SelectedTime, Converter={x:Static ObjectConverters.IsNotNull}}" />
</Style>
<Style Selector="^:pointerover /template/ Button#PART_Button">
<Setter Property="IsVisible" Value="{Binding $parent[u:TimePicker].SelectedTime, Converter={x:Static ObjectConverters.IsNull}}" />
</Style>
</Style>
<!-- Disabled State -->
<Style Selector="^:disabled">
<Style Selector="^ /template/ Border#Background">
<Setter Property="Background" Value="{DynamicResource CalendarDatePickerDisabledBackground}" />
</Style>
<Style Selector="^ /template/ Button#PART_Button">
<Setter Property="Foreground" Value="{DynamicResource CalendarDatePickerDisabledIconForeground}" />
</Style>
<Style Selector="^ /template/ TextBox#PART_TextBox">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
</Style>
<!-- Focused State -->
<Style Selector="^ /template/ Border#Background:focus">
<Setter Property="BorderBrush" Value="{DynamicResource CalendarDatePickerFocusBorderBrush}" />
</Style>
<Style Selector="^ /template/ Border#Background:focus-within">
<Setter Property="BorderBrush" Value="{DynamicResource CalendarDatePickerFocusBorderBrush}" />
</Style>
</ControlTheme>
</ResourceDictionary>

View File

@@ -5,8 +5,11 @@
<ResourceInclude Source="Banner.axaml" />
<ResourceInclude Source="ButtonGroup.axaml" />
<ResourceInclude Source="Breadcrumb.axaml" />
<ResourceInclude Source="Calendar.axaml" />
<ResourceInclude Source="ControlClassesInput.axaml" />
<ResourceInclude Source="Clock.axaml" />
<ResourceInclude Source="DatePicker.axaml" />
<ResourceInclude Source="DateRangePicker.axaml" />
<ResourceInclude Source="Dialog.axaml" />
<ResourceInclude Source="DialogShared.axaml" />
<ResourceInclude Source="DisableContainer.axaml" />

View File

@@ -0,0 +1,41 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--CalenderDayButton-->
<SolidColorBrush x:Key="CalenderDayBackground" Color="Transparent" />
<SolidColorBrush x:Key="CalenderDayBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="CalenderDayInRangeBackground" Opacity="0.2" Color="#54A9FF" />
<SolidColorBrush x:Key="CalenderDayTodayBackground" Opacity="0.12" Color="White" />
<SolidColorBrush x:Key="CalenderDayTodayForeground" Color="#54A9FF" />
<SolidColorBrush x:Key="CalenderDayNotCurrentMonthForeground" Color="#555B61" />
<SolidColorBrush x:Key="CalenderDayPointeroverBackground" Opacity="0.12" Color="White" />
<SolidColorBrush x:Key="CalenderDayPressedBackground" Opacity="0.16" Color="White" />
<SolidColorBrush x:Key="CalenderDaySelectedBackground" Color="#54A9FF" />
<SolidColorBrush x:Key="CalenderDaySelectedForeground" Color="White" />
<SolidColorBrush x:Key="CalenderDaySelectedPointeroverBackground" Color="#7FC1FF" />
<SolidColorBrush x:Key="CalenderDaySelectedPressedBackground" Color="#A9D7FF" />
<SolidColorBrush x:Key="CalenderDayStartEndDateBackground" Color="#54A9FF" />
<SolidColorBrush x:Key="CalenderDayStartEndDateBorderBrush" Color="#54A9FF" />
<SolidColorBrush x:Key="CalenderDayStartEndDatePointeroverBackground" Color="#7FC1FF" />
<SolidColorBrush x:Key="CalenderDayStartEndDatePressedBackground" Color="#A9D7FF" />
<SolidColorBrush x:Key="CalenderDayStartEndDateInRangeBackground" Color="#A9D7FF" />
<SolidColorBrush x:Key="CalenderDayPreviewStartEndDateBackground" Opacity="0.3" Color="#54A9FF" />
<SolidColorBrush x:Key="CalenderDayPreviewStartEndDatePressedBackground" Opacity="0.4" Color="#54A9FF" />
<SolidColorBrush x:Key="CalenderDayBlackoutForeground" Opacity="0.35" Color="#F9F9F9" />
<SolidColorBrush x:Key="CalenderDayBlackoutBackground" Opacity="0.04" Color="#E6E8EA" />
<SolidColorBrush x:Key="CalenderDayBlackoutTodayBackground" Color="#135CB8" />
<!--CalenderYearButton-->
<SolidColorBrush x:Key="CalenderYearBackground" Color="Transparent" />
<SolidColorBrush x:Key="CalenderYearBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="CalenderYearPointeroverBackground" Opacity="0.12" Color="White" />
<SolidColorBrush x:Key="CalenderYearPressedBackground" Opacity="0.16" Color="White" />
<SolidColorBrush x:Key="CalenderYearSelectedBackground" Color="#54A9FF" />
<SolidColorBrush x:Key="CalenderYearSelectedForeground" Color="White" />
<SolidColorBrush x:Key="CalenderYearSelectedPointeroverBackground" Color="#7FC1FF" />
<!--CalenderView-->
<SolidColorBrush x:Key="CalenderViewBackground" Color="Transparent" />
</ResourceDictionary>

View File

@@ -1,9 +1,9 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<ResourceDictionary.MergedDictionaries>
<MergeResourceInclude Source="Badge.axaml" />
<MergeResourceInclude Source="Banner.axaml" />
<MergeResourceInclude Source="ButtonGroup.axaml" />
<MergeResourceInclude Source="DatePicker.axaml" />
<MergeResourceInclude Source="Dialog.axaml" />
<MergeResourceInclude Source="Divider.axaml" />
<MergeResourceInclude Source="DualBadge.axaml" />
@@ -16,6 +16,6 @@
<MergeResourceInclude Source="TagInput.axaml" />
<MergeResourceInclude Source="Timeline.axaml" />
<MergeResourceInclude Source="Skeleton.axaml" />
<MergeResourceInclude Source="TimeBox.axaml"/>
<MergeResourceInclude Source="TimeBox.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</ResourceDictionary>

View File

@@ -0,0 +1,41 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--CalenderDayButton-->
<SolidColorBrush x:Key="CalenderDayBackground" Color="Transparent" />
<SolidColorBrush x:Key="CalenderDayBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="CalenderDayInRangeBackground" Color="#EAF5FF" />
<SolidColorBrush x:Key="CalenderDayTodayBackground" Opacity="0.05" Color="#2E3238" />
<SolidColorBrush x:Key="CalenderDayTodayForeground" Color="#0077FA" />
<SolidColorBrush x:Key="CalenderDayNotCurrentMonthForeground" Color="#A7ABB0" />
<SolidColorBrush x:Key="CalenderDayPointeroverBackground" Opacity="0.05" Color="#2E3238" />
<SolidColorBrush x:Key="CalenderDayPressedBackground" Opacity="0.09" Color="#2E3238" />
<SolidColorBrush x:Key="CalenderDaySelectedBackground" Color="#0077FA" />
<SolidColorBrush x:Key="CalenderDaySelectedForeground" Color="White" />
<SolidColorBrush x:Key="CalenderDaySelectedPointeroverBackground" Color="#0062D6" />
<SolidColorBrush x:Key="CalenderDaySelectedPressedBackground" Color="#004FB3" />
<SolidColorBrush x:Key="CalenderDayStartEndDateBackground" Color="#0077FA" />
<SolidColorBrush x:Key="CalenderDayStartEndDateBorderBrush" Color="#0077FA" />
<SolidColorBrush x:Key="CalenderDayStartEndDatePointeroverBackground" Color="#0062D6" />
<SolidColorBrush x:Key="CalenderDayStartEndDatePressedBackground" Color="#004FB3" />
<SolidColorBrush x:Key="CalenderDayStartEndDateInRangeBackground" Color="#004FB3" />
<SolidColorBrush x:Key="CalenderDayPreviewStartEndDateBackground" Color="#CBE7FE" />
<SolidColorBrush x:Key="CalenderDayPreviewStartEndDatePressedBackground" Color="#98CDFD" />
<SolidColorBrush x:Key="CalenderDayBlackoutForeground" Opacity="0.35" Color="#1C1F23" />
<SolidColorBrush x:Key="CalenderDayBlackoutBackground" Opacity="0.02" Color="#2E3238" />
<SolidColorBrush x:Key="CalenderDayBlackoutTodayBackground" Color="#98CDFD" />
<!--CalenderYearButton-->
<SolidColorBrush x:Key="CalenderYearBackground" Color="Transparent" />
<SolidColorBrush x:Key="CalenderYearBorderBrush" Color="Transparent" />
<SolidColorBrush x:Key="CalenderYearPointeroverBackground" Opacity="0.05" Color="#2E3238" />
<SolidColorBrush x:Key="CalenderYearPressedBackground" Opacity="0.09" Color="#2E3238" />
<SolidColorBrush x:Key="CalenderYearSelectedBackground" Color="#0077FA" />
<SolidColorBrush x:Key="CalenderYearSelectedForeground" Color="White" />
<SolidColorBrush x:Key="CalenderYearSelectedPointeroverBackground" Color="#0062D6" />
<!--CalenderView-->
<SolidColorBrush x:Key="CalenderViewBackground" Color="Transparent" />
</ResourceDictionary>

View File

@@ -1,9 +1,9 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<ResourceDictionary.MergedDictionaries>
<MergeResourceInclude Source="Badge.axaml" />
<MergeResourceInclude Source="Banner.axaml" />
<MergeResourceInclude Source="ButtonGroup.axaml" />
<MergeResourceInclude Source="DatePicker.axaml" />
<MergeResourceInclude Source="Dialog.axaml" />
<MergeResourceInclude Source="Divider.axaml" />
<MergeResourceInclude Source="DualBadge.axaml" />
@@ -16,6 +16,6 @@
<MergeResourceInclude Source="TagInput.axaml" />
<MergeResourceInclude Source="Timeline.axaml" />
<MergeResourceInclude Source="Skeleton.axaml" />
<MergeResourceInclude Source="TimeBox.axaml"/>
<MergeResourceInclude Source="TimeBox.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</ResourceDictionary>

View File

@@ -0,0 +1,30 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!--CalenderDayButton-->
<x:Double x:Key="CalenderDayMinWidth">32</x:Double>
<x:Double x:Key="CalenderDayMinHeight">32</x:Double>
<Thickness x:Key="CalenderDayMargin">0 2</Thickness>
<Thickness x:Key="CalenderDayBorderThickness">1</Thickness>
<CornerRadius x:Key="CalenderDayCornerRadius">3</CornerRadius>
<CornerRadius x:Key="CalenderDayInRangeCornerRadius">0</CornerRadius>
<Thickness x:Key="CalenderDayNoBorderThickness">0</Thickness>
<CornerRadius x:Key="CalenderDayStartDateCornerRadius">3 0 0 3</CornerRadius>
<CornerRadius x:Key="CalenderDayEndDateCornerRadius">0 3 3 0</CornerRadius>
<!--CalenderYearButton-->
<x:Double x:Key="CalenderYearMinWidth">32</x:Double>
<x:Double x:Key="CalenderYearMinHeight">32</x:Double>
<Thickness x:Key="CalenderYearMargin">0 2</Thickness>
<Thickness x:Key="CalenderYearBorderThickness">1</Thickness>
<CornerRadius x:Key="CalenderYearCornerRadius">3</CornerRadius>
<Thickness x:Key="CalenderYearSelectedBorderThickness">0</Thickness>
<!--CalenderView-->
<x:Double x:Key="CalenderViewMinWidth">260</x:Double>
<x:Double x:Key="CalenderViewMinHeight">260</x:Double>
<x:Double x:Key="CalenderViewPathIconWidth">12</x:Double>
<x:Double x:Key="CalenderViewPathIconHeight">12</x:Double>
<StreamGeometry x:Key="CalendarViewFastForwardGlyph">M12.6185 4.39653C13.1272 4.92524 13.1272 5.78245 12.6185 6.31116L7.14483 12L12.6185 17.6888C13.1272 18.2176 13.1272 19.0748 12.6185 19.6035C12.1098 20.1322 11.285 20.1322 10.7763 19.6035L4.38153 12.9573C3.87282 12.4286 3.87282 11.5714 4.38153 11.0427L10.7763 4.39653C11.285 3.86782 12.1098 3.86782 12.6185 4.39653Z M19.6185 4.39653C20.1272 4.92524 20.1272 5.78245 19.6185 6.31116L14.1448 12L19.6185 17.6888C20.1272 18.2176 20.1272 19.0748 19.6185 19.6035C19.1098 20.1322 18.285 20.1322 17.7763 19.6035L11.3815 12.9573C10.8728 12.4286 10.8728 11.5714 11.3815 11.0427L17.7763 4.39653C18.285 3.86782 19.1098 3.86782 19.6185 4.39653Z</StreamGeometry>
<StreamGeometry x:Key="CalendarViewFastBackwardGlyph">M4.38153 4.39653C4.89024 3.86782 5.71502 3.86782 6.22373 4.39653L12.6185 11.0427C13.1272 11.5714 13.1272 12.4286 12.6185 12.9573L6.22373 19.6035C5.71502 20.1322 4.89024 20.1322 4.38153 19.6035C3.87282 19.0748 3.87282 18.2176 4.38153 17.6888L9.85517 12L4.38153 6.31116C3.87282 5.78245 3.87282 4.92524 4.38153 4.39653Z M11.3815 4.39653C11.8902 3.86782 12.715 3.86782 13.2237 4.39653L19.6185 11.0427C20.1272 11.5714 20.1272 12.4286 19.6185 12.9573L13.2237 19.6035C12.715 20.1322 11.8902 20.1322 11.3815 19.6035C10.8728 19.0748 10.8728 18.2176 11.3815 17.6888L16.8552 12L11.3815 6.31116C10.8728 5.78245 10.8728 4.92524 11.3815 4.39653Z</StreamGeometry>
</ResourceDictionary>

View File

@@ -1,9 +1,9 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<ResourceDictionary.MergedDictionaries>
<MergeResourceInclude Source="Badge.axaml" />
<MergeResourceInclude Source="Banner.axaml" />
<MergeResourceInclude Source="ButtonGroup.axaml" />
<MergeResourceInclude Source="DatePicker.axaml" />
<MergeResourceInclude Source="Dialog.axaml" />
<MergeResourceInclude Source="DialogShared.axaml" />
<MergeResourceInclude Source="Divider.axaml" />
@@ -19,6 +19,6 @@
<MergeResourceInclude Source="Skeleton.axaml" />
<MergeResourceInclude Source="ThemeSelector.axaml" />
<MergeResourceInclude Source="ToolBar.axaml" />
<MergeResourceInclude Source="TimeBox.axaml"/>
<MergeResourceInclude Source="TimeBox.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</ResourceDictionary>

View File

@@ -0,0 +1,91 @@
namespace Ursa.Controls;
public sealed class CalendarContext(int? year = null, int? month = null, int? startYear = null, int? endYear = null): IComparable<CalendarContext>
{
public int? Year { get; } = year;
public int? Month { get; } = month;
public int? StartYear { get; } = startYear;
public int? EndYear { get; } = endYear;
public CalendarContext Clone()
{
return new CalendarContext(Year, Month, StartYear, EndYear);
}
public static CalendarContext Today()
{
return new CalendarContext(DateTime.Today.Year, DateTime.Today.Month);
}
public CalendarContext With(int? year = null, int? month = null, int? startYear = null, int? endYear = null)
{
return new CalendarContext(year ?? Year, month ?? Month, startYear ?? StartYear, endYear ?? EndYear);
}
public CalendarContext NextMonth()
{
var year = Year;
var month = Month;
if (month == 12)
{
year++;
month = 1;
}
else
{
month++;
}
if (month is null)
{
month = 1;
}
return new CalendarContext(year, month, StartYear, EndYear);
}
public CalendarContext PreviousMonth()
{
var year = Year;
var month = Month;
if (month == 1)
{
year--;
month = 12;
}
else
{
month--;
}
if (month is null)
{
month = 1;
}
return new CalendarContext(year, month, StartYear, EndYear);
}
public CalendarContext NextYear()
{
return new CalendarContext(Year + 1, Month, StartYear, EndYear);
}
public CalendarContext PreviousYear()
{
return new CalendarContext(Year - 1, Month, StartYear, EndYear);
}
public int CompareTo(CalendarContext? other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
var yearComparison = Nullable.Compare(Year, other.Year);
if (yearComparison != 0) return yearComparison;
return Nullable.Compare(Month, other.Month);
}
public override string ToString()
{
return
$"Start: {StartYear?.ToString() ?? "null"}, End: {EndYear?.ToString() ?? "null"}, Year: {Year?.ToString() ?? "null"}, Month: {Month?.ToString() ?? "null"}";
}
}

View File

@@ -0,0 +1,205 @@
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Common;
namespace Ursa.Controls;
[PseudoClasses(PseudoClassName.PC_Pressed, PseudoClassName.PC_Selected,
PC_StartDate, PC_EndDate, PC_PreviewStartDate, PC_PreviewEndDate, PC_InRange, PC_Today, PC_Blackout,
PC_NotCurrentMonth)]
public class CalendarDayButton : ContentControl
{
public const string PC_StartDate = ":start-date";
public const string PC_EndDate = ":end-date";
public const string PC_PreviewStartDate = ":preview-start-date";
public const string PC_PreviewEndDate = ":preview-end-date";
public const string PC_InRange = ":in-range";
public const string PC_Today = ":today";
public const string PC_NotCurrentMonth = ":not-current-month";
public const string PC_Blackout = ":blackout";
private static HashSet<string> _pseudoClasses =
[
PseudoClassName.PC_Selected, PC_StartDate, PC_EndDate, PC_PreviewStartDate, PC_PreviewEndDate, PC_InRange
];
public static readonly RoutedEvent<CalendarDayButtonEventArgs> DateSelectedEvent =
RoutedEvent.Register<CalendarDayButton, CalendarDayButtonEventArgs>(
nameof(DateSelected), RoutingStrategies.Bubble);
public static readonly RoutedEvent<CalendarDayButtonEventArgs> DatePreviewedEvent =
RoutedEvent.Register<CalendarDayButton, CalendarDayButtonEventArgs>(
nameof(DatePreviewed), RoutingStrategies.Bubble);
private bool _isBlackout;
private bool _isEndDate;
private bool _isInRange;
private bool _isNotCurrentMonth;
private bool _isPreviewEndDate;
private bool _isPreviewStartDate;
private bool _isSelected;
private bool _isStartDate;
private bool _isToday;
static CalendarDayButton()
{
PressedMixin.Attach<CalendarDayButton>();
}
// internal CalendarDisplayControl? Owner { get; set; }
public bool IsToday
{
get => _isToday;
set
{
_isToday = value;
PseudoClasses.Set(PC_Today, value);
}
}
public bool IsStartDate
{
get => _isStartDate;
set
{
_isStartDate = value;
SetPseudoClass(PC_StartDate, value);
}
}
public bool IsEndDate
{
get => _isEndDate;
set
{
_isEndDate = value;
SetPseudoClass(PC_EndDate, value);
}
}
public bool IsPreviewStartDate
{
get => _isPreviewStartDate;
set
{
_isPreviewStartDate = value;
SetPseudoClass(PC_PreviewStartDate, value);
}
}
public bool IsPreviewEndDate
{
get => _isPreviewEndDate;
set
{
_isPreviewEndDate = value;
SetPseudoClass(PC_PreviewEndDate, value);
}
}
public bool IsInRange
{
get => _isInRange;
set
{
_isInRange = value;
SetPseudoClass(PC_InRange, value);
}
}
public bool IsSelected
{
get => _isSelected;
set
{
_isSelected = value;
SetPseudoClass(PseudoClassName.PC_Selected, value);
}
}
/// <summary>
/// Notice: IsBlackout is not equivalent to not IsEnabled. Blackout dates still react to pointerover actions.
/// </summary>
public bool IsBlackout
{
get => _isBlackout;
set
{
_isBlackout = value;
PseudoClasses.Set(PC_Blackout, value);
}
}
/// <summary>
/// Notice: IsNotCurrentMonth is not equivalent to not IsEnabled. Not current month dates still react to pointerover
/// and press action.
/// </summary>
public bool IsNotCurrentMonth
{
get => _isNotCurrentMonth;
set
{
_isNotCurrentMonth = value;
PseudoClasses.Set(PC_NotCurrentMonth, value);
}
}
public event EventHandler<CalendarDayButtonEventArgs> DateSelected
{
add => AddHandler(DateSelectedEvent, value);
remove => RemoveHandler(DateSelectedEvent, value);
}
public event EventHandler<CalendarDayButtonEventArgs> DatePreviewed
{
add => AddHandler(DateSelectedEvent, value);
remove => RemoveHandler(DateSelectedEvent, value);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (DataContext is DateTime d)
RaiseEvent(new CalendarDayButtonEventArgs(d) { RoutedEvent = DateSelectedEvent, Source = this });
}
protected override void OnPointerEntered(PointerEventArgs e)
{
base.OnPointerEntered(e);
if (DataContext is DateTime d)
RaiseEvent(new CalendarDayButtonEventArgs(d) { RoutedEvent = DatePreviewedEvent, Source = this });
}
internal void ResetSelection()
{
foreach (var pc in _pseudoClasses)
{
PseudoClasses.Set(pc, false);
}
}
private void SetPseudoClass(string s, bool value)
{
if (_pseudoClasses.Contains(s) && value)
{
foreach (var pc in _pseudoClasses)
{
PseudoClasses.Set(pc, false);
}
}
PseudoClasses.Set(s, value);
}
}

View File

@@ -0,0 +1,8 @@
using Avalonia.Interactivity;
namespace Ursa.Controls;
public class CalendarDayButtonEventArgs(DateTime? date) : RoutedEventArgs
{
public DateTime? Date { get; private set; } = date;
}

View File

@@ -0,0 +1,617 @@
using System.Diagnostics;
using System.Globalization;
using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Irihi.Avalonia.Shared.Helpers;
using Calendar = System.Globalization.Calendar;
namespace Ursa.Controls;
[TemplatePart(PART_FastNextButton, typeof(Button))]
[TemplatePart(PART_FastPreviousButton, typeof(Button))]
[TemplatePart(PART_NextButton, typeof(Button))]
[TemplatePart(PART_PreviousButton, typeof(Button))]
[TemplatePart(PART_YearButton, typeof(Button))]
[TemplatePart(PART_MonthButton, typeof(Button))]
[TemplatePart(PART_HeaderButton, typeof(Button))]
[TemplatePart(PART_MonthGrid, typeof(Grid))]
[TemplatePart(PART_YearGrid, typeof(Grid))]
[PseudoClasses(PC_Month)]
public class CalendarView : TemplatedControl
{
public const string PART_FastNextButton = "PART_FastNextButton";
public const string PART_FastPreviousButton = "PART_FastPreviousButton";
public const string PART_NextButton = "PART_NextButton";
public const string PART_PreviousButton = "PART_PreviousButton";
public const string PART_YearButton = "PART_YearButton";
public const string PART_MonthButton = "PART_MonthButton";
public const string PART_HeaderButton = "PART_HeaderButton";
public const string PART_MonthGrid = "PART_MonthGrid";
public const string PART_YearGrid = "PART_YearGrid";
public const string PC_Month = ":month";
private const string ShortestDayName = "ShortestDayName";
internal static readonly DirectProperty<CalendarView, CalendarViewMode> ModeProperty =
AvaloniaProperty.RegisterDirect<CalendarView, CalendarViewMode>(
nameof(Mode), o => o.Mode, (o, v) => o.Mode = v);
public static readonly StyledProperty<bool> IsTodayHighlightedProperty =
DatePickerBase.IsTodayHighlightedProperty.AddOwner<CalendarView>();
public static readonly StyledProperty<DayOfWeek> FirstDayOfWeekProperty =
DatePickerBase.FirstDayOfWeekProperty.AddOwner<CalendarView>();
private readonly Calendar _calendar = new GregorianCalendar();
private Button? _fastNextButton;
private Button? _fastPreviousButton;
private Button? _headerButton;
private CalendarViewMode _mode;
private Button? _monthButton;
private Grid? _monthGrid;
private Button? _nextButton;
private Button? _previousButton;
private Button? _yearButton;
private Grid? _yearGrid;
private DateTime? _start;
private DateTime? _end;
private DateTime? _previewStart;
private DateTime? _previewEnd;
static CalendarView()
{
FirstDayOfWeekProperty.Changed.AddClassHandler<CalendarView, DayOfWeek>((view, args) =>
view.OnFirstDayOfWeekChanged(args));
ModeProperty.Changed.AddClassHandler<CalendarView, CalendarViewMode>((view, args) =>
{
view.PseudoClasses.Set(PC_Month, args.NewValue.Value == CalendarViewMode.Month);
});
ContextDateProperty.Changed.AddClassHandler<CalendarView, CalendarContext>((view, args) =>
view.OnContextDateChanged(args));
}
private void OnContextDateChanged(AvaloniaPropertyChangedEventArgs<CalendarContext> args)
{
Debug.WriteLine(this.Name + " " + args.NewValue.Value);
if (!_dateContextSyncing)
{
ContextDateChanged?.Invoke(this, args.NewValue.Value);
}
//UpdateDayButtons();
//UpdateYearButtons();
}
internal CalendarViewMode Mode
{
get => _mode;
set => SetAndRaise(ModeProperty, ref _mode, value);
}
private CalendarContext _contextDate = new();
public static readonly DirectProperty<CalendarView, CalendarContext> ContextDateProperty = AvaloniaProperty.RegisterDirect<CalendarView, CalendarContext>(
nameof(ContextDate), o => o.ContextDate, (o, v) => o.ContextDate = v);
public CalendarContext ContextDate
{
get => _contextDate;
internal set => SetAndRaise(ContextDateProperty, ref _contextDate, value);
}
public bool IsTodayHighlighted
{
get => GetValue(IsTodayHighlightedProperty);
set => SetValue(IsTodayHighlightedProperty, value);
}
public DayOfWeek FirstDayOfWeek
{
get => GetValue(FirstDayOfWeekProperty);
set => SetValue(FirstDayOfWeekProperty, value);
}
public event EventHandler<CalendarDayButtonEventArgs>? DateSelected;
public event EventHandler<CalendarDayButtonEventArgs>? DatePreviewed;
internal event EventHandler<CalendarContext>? ContextDateChanged;
private void OnFirstDayOfWeekChanged(AvaloniaPropertyChangedEventArgs<DayOfWeek> args)
{
UpdateMonthViewHeader(args.NewValue.Value);
UpdateDayButtons();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
Button.ClickEvent.RemoveHandler(OnHeaderYearButtonClick, _yearButton);
Button.ClickEvent.RemoveHandler(OnHeaderMonthButtonClick, _monthButton);
Button.ClickEvent.RemoveHandler(OnHeaderButtonClick, _headerButton);
Button.ClickEvent.RemoveHandler(OnFastPrevious, _fastPreviousButton);
Button.ClickEvent.RemoveHandler(OnPrevious, _previousButton);
Button.ClickEvent.RemoveHandler(OnNext, _nextButton);
Button.ClickEvent.RemoveHandler(OnFastNext, _fastNextButton);
_monthGrid = e.NameScope.Find<Grid>(PART_MonthGrid);
_yearGrid = e.NameScope.Find<Grid>(PART_YearGrid);
_yearButton = e.NameScope.Find<Button>(PART_YearButton);
_monthButton = e.NameScope.Find<Button>(PART_MonthButton);
_headerButton = e.NameScope.Find<Button>(PART_HeaderButton);
_fastPreviousButton = e.NameScope.Find<Button>(PART_FastPreviousButton);
_previousButton = e.NameScope.Find<Button>(PART_PreviousButton);
_nextButton = e.NameScope.Find<Button>(PART_NextButton);
_fastNextButton = e.NameScope.Find<Button>(PART_FastNextButton);
Button.ClickEvent.AddHandler(OnHeaderYearButtonClick, _yearButton);
Button.ClickEvent.AddHandler(OnHeaderMonthButtonClick, _monthButton);
Button.ClickEvent.AddHandler(OnHeaderButtonClick, _headerButton);
Button.ClickEvent.AddHandler(OnFastPrevious, _fastPreviousButton);
Button.ClickEvent.AddHandler(OnPrevious, _previousButton);
Button.ClickEvent.AddHandler(OnNext, _nextButton);
Button.ClickEvent.AddHandler(OnFastNext, _fastNextButton);
ContextDate = new CalendarContext(DateTime.Today.Year, DateTime.Today.Month);
PseudoClasses.Set(PC_Month, Mode == CalendarViewMode.Month);
InitializeGridButtons();
UpdateDayButtons();
UpdateYearButtons();
}
private void OnFastNext(object sender, RoutedEventArgs e)
{
if (Mode == CalendarViewMode.Month)
{
ContextDate = ContextDate.With(year: ContextDate.Year + 1);
UpdateDayButtons();
}
}
private void OnNext(object sender, RoutedEventArgs e)
{
if (Mode == CalendarViewMode.Month)
{
ContextDate = ContextDate.NextMonth();
UpdateDayButtons();
}
else if (Mode == CalendarViewMode.Year)
{
ContextDate = ContextDate.NextYear();
UpdateYearButtons();
}
else if (Mode == CalendarViewMode.Decade)
{
ContextDate = ContextDate.With(startYear: ContextDate.StartYear + 10, endYear: ContextDate.EndYear + 10);
UpdateYearButtons();
}
else if (Mode == CalendarViewMode.Century)
{
ContextDate = ContextDate.With(startYear: ContextDate.StartYear + 100, endYear: ContextDate.EndYear + 100);
UpdateYearButtons();
}
}
private void OnPrevious(object sender, RoutedEventArgs e)
{
if (Mode == CalendarViewMode.Month)
{
ContextDate = ContextDate.PreviousMonth();
UpdateDayButtons();
}
else if (Mode == CalendarViewMode.Year)
{
ContextDate = ContextDate.With(year: ContextDate.Year - 1);
UpdateYearButtons();
}
else if (Mode == CalendarViewMode.Decade)
{
ContextDate = ContextDate.With(startYear: ContextDate.StartYear - 10, endYear: ContextDate.EndYear - 10);
UpdateYearButtons();
}
else if (Mode == CalendarViewMode.Century)
{
ContextDate = ContextDate.With(startYear: ContextDate.StartYear - 100, endYear: ContextDate.EndYear - 100);
UpdateYearButtons();
}
}
private void OnFastPrevious(object sender, RoutedEventArgs e)
{
if (Mode == CalendarViewMode.Month)
{
ContextDate = ContextDate.PreviousYear();
UpdateDayButtons();
}
}
/// <summary>
/// Rule:
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnHeaderButtonClick(object sender, RoutedEventArgs e)
{
// Header button should be hidden in Month mode.
if (Mode == CalendarViewMode.Month) return;
if (Mode == CalendarViewMode.Year)
{
Mode = CalendarViewMode.Decade;
var range = DateTimeHelper.GetDecadeViewRangeByYear(ContextDate.Year!.Value);
_dateContextSyncing = true;
ContextDate = ContextDate.With(startYear: range.start, endYear: range.end);
_dateContextSyncing = false;
UpdateYearButtons();
return;
}
if (Mode == CalendarViewMode.Decade)
{
Mode = CalendarViewMode.Century;
var range = DateTimeHelper.GetCenturyViewRangeByYear(ContextDate.StartYear!.Value);
_dateContextSyncing = true;
ContextDate = ContextDate.With(startYear: range.start, endYear: range.end);
_dateContextSyncing = false;
UpdateYearButtons();
return;
}
if (Mode == CalendarViewMode.Century) return;
}
/// <summary>
/// Generate Buttons and labels for MonthView.
/// Generate Buttons for YearView.
/// This method should be called only once.
/// </summary>
private void InitializeGridButtons()
{
// Generate Day titles (Sun, Mon, Tue, Wed, Thu, Fri, Sat) based on FirstDayOfWeek and culture.
var count = 7 + 7 * 7;
var children = new List<Control>(count);
var dayOfWeek = (int)FirstDayOfWeek;
var info = DateTimeHelper.GetCurrentDateTimeFormatInfo();
for (var i = 0; i < 7; i++)
{
var d = (dayOfWeek + i) % DateTimeHelper.NumberOfDaysPerWeek;
var cell = new TextBlock { Text = info.ShortestDayNames[d], Tag = ShortestDayName };
cell.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Center);
cell.SetValue(Grid.RowProperty, 0);
cell.SetValue(Grid.ColumnProperty, i);
children.Add(cell);
}
// Generate day buttons.
for (var i = 2; i < DateTimeHelper.NumberOfWeeksPerMonth + 2; i++)
for (var j = 0; j < DateTimeHelper.NumberOfDaysPerWeek; j++)
{
var cell = new CalendarDayButton();
cell.SetValue(Grid.RowProperty, i);
cell.SetValue(Grid.ColumnProperty, j);
cell.AddHandler(CalendarDayButton.DateSelectedEvent, OnCellDateSelected);
cell.AddHandler(CalendarDayButton.DatePreviewedEvent, OnCellDatePreviewed);
children.Add(cell);
}
_monthGrid?.Children.AddRange(children);
// Generate month/year buttons.
for (var i = 0; i < 12; i++)
{
var button = new CalendarYearButton();
Grid.SetRow(button, i / 3);
Grid.SetColumn(button, i % 3);
button.AddHandler(CalendarYearButton.ItemSelectedEvent, OnYearItemSelected);
_yearGrid?.Children.Add(button);
}
}
internal void UpdateDayButtons()
{
if (_monthGrid is null || Mode != CalendarViewMode.Month) return;
var children = _monthGrid.Children;
var info = DateTimeHelper.GetCurrentDateTimeFormatInfo();
var date = new DateTime(ContextDate.Year ?? ContextDate.StartYear.Value, ContextDate.Month.Value, 1);
var dayBefore = PreviousMonthDays(date);
var dateToSet = date.GetFirstDayOfMonth().AddDays(-dayBefore);
for (var i = 7; i < children.Count; i++)
{
var day = dateToSet;
var cell = children[i] as CalendarDayButton;
if (cell is null) continue;
cell.DataContext = day;
if (IsTodayHighlighted) cell.IsToday = day == DateTime.Today;
cell.Content = day.Day.ToString(info);
dateToSet = dateToSet.AddDays(1);
}
FadeOutDayButtons();
MarkDates(_start, _end, _previewStart, _previewEnd);
UpdateHeaderButtons();
}
private void UpdateYearButtons()
{
if (_yearGrid is null) return;
var mode = Mode;
var contextDate = ContextDate;
if (mode == CalendarViewMode.Century && contextDate.StartYear.HasValue)
{
var range = DateTimeHelper.GetCenturyViewRangeByYear(contextDate.StartYear.Value);
var start = range.start - 10;
for (var i = 0; i < 12; i++)
{
var child = _yearGrid.Children[i] as CalendarYearButton;
child?.SetContext(CalendarViewMode.Century,
new CalendarContext(startYear: start, endYear: start + 10));
start += 10;
}
}
else if (mode == CalendarViewMode.Decade && contextDate.StartYear.HasValue)
{
var range = DateTimeHelper.GetDecadeViewRangeByYear(contextDate.StartYear.Value);
var year = range.start - 1;
for (var i = 0; i < 12; i++)
{
var child = _yearGrid.Children[i] as CalendarYearButton;
child?.SetContext(CalendarViewMode.Decade,
new CalendarContext(year: year));
year++;
}
}
else if (mode == CalendarViewMode.Year)
{
for (var i = 0; i < 12; i++)
{
var child = _yearGrid.Children[i] as CalendarYearButton;
child?.SetContext(CalendarViewMode.Year, new CalendarContext(month: i + 1));
}
}
UpdateHeaderButtons();
}
private void FadeOutDayButtons()
{
if (_monthGrid is null) return;
var children = _monthGrid.Children;
for (var i = 7; i < children.Count; i++)
if (children[i] is CalendarDayButton { DataContext: DateTime d } button)
button.IsNotCurrentMonth = d.Month != ContextDate.Month;
}
private void UpdateMonthViewHeader(DayOfWeek day)
{
var dayOfWeek = (int)day;
var info = DateTimeHelper.GetCurrentDateTimeFormatInfo();
var texts = _monthGrid?.Children.Where(a => a is TextBlock { Tag: ShortestDayName }).ToList();
if (texts is not null)
for (var i = 0; i < 7; i++)
{
var d = (dayOfWeek + i) % DateTimeHelper.NumberOfDaysPerWeek;
texts[i].SetValue(TextBlock.TextProperty, info.ShortestDayNames[d]);
texts[i].SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Center);
texts[i].SetValue(Grid.RowProperty, 0);
texts[i].SetValue(Grid.ColumnProperty, i);
}
}
private int PreviousMonthDays(DateTime date)
{
var firstDay = date.GetFirstDayOfMonth();
var dayOfWeek = _calendar.GetDayOfWeek(firstDay);
var firstDayOfWeek = FirstDayOfWeek;
var i = (dayOfWeek - firstDayOfWeek + DateTimeHelper.NumberOfDaysPerWeek) % DateTimeHelper.NumberOfDaysPerWeek;
return i == 0 ? DateTimeHelper.NumberOfDaysPerWeek : i;
}
private void OnCellDatePreviewed(object sender, CalendarDayButtonEventArgs e)
{
DatePreviewed?.Invoke(this, e);
}
private void OnCellDateSelected(object sender, CalendarDayButtonEventArgs e)
{
if (e.Date.HasValue && e.Date.Value.Month != ContextDate.Month)
{
ContextDate = ContextDate.With(year: e.Date.Value.Year, month: e.Date.Value.Month);
UpdateDayButtons();
}
DateSelected?.Invoke(this, e);
}
/// <summary>
/// Click on Month Header button. Calendar switch from month mode to year mode.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnHeaderMonthButtonClick(object sender, RoutedEventArgs e)
{
SetCurrentValue(ModeProperty, CalendarViewMode.Year);
UpdateYearButtons();
}
/// <summary>
/// Click on Year Header button. Calendar switch from month mode to decade mode.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnHeaderYearButtonClick(object sender, RoutedEventArgs e)
{
if (_yearGrid is null) return;
SetCurrentValue(ModeProperty, CalendarViewMode.Decade);
var range = DateTimeHelper.GetDecadeViewRangeByYear(ContextDate.Year!.Value);
_dateContextSyncing = true;
ContextDate = ContextDate.With(startYear: range.start, endYear: range.end);
_dateContextSyncing = false;
UpdateYearButtons();
}
/// <summary>
/// Click on CalendarYearButton in YearView.
/// Mode switch rules are:
/// 1. Month -> Not supported, buttons are hidden.
/// 2. Year -> Month: Set the date to the selected year and switch to Month mode.
/// 3. Decade -> Year: Set the date to the selected year and switch to Year mode.
/// 4. Century -> Decade: Set the date to the selected year and switch to Decade mode.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnYearItemSelected(object sender, CalendarYearButtonEventArgs e)
{
if (_yearGrid is null) return;
var buttons = _yearGrid.Children.OfType<CalendarYearButton>().ToList();
if (Mode == CalendarViewMode.Century)
{
Mode = CalendarViewMode.Decade;
ContextDate = e.Context.With(year: null);
}
else if (Mode == CalendarViewMode.Decade)
{
Mode = CalendarViewMode.Year;
ContextDate = e.Context.Clone();
}
else if (Mode == CalendarViewMode.Year)
{
Mode = CalendarViewMode.Month;
ContextDate = ContextDate.With(null, e.Context.Month, null, null);
UpdateDayButtons();
}
else if (Mode == CalendarViewMode.Month)
{
return;
}
UpdateHeaderButtons();
UpdateYearButtons();
}
private void UpdateHeaderButtons()
{
if (Mode == CalendarViewMode.Century)
{
IsVisibleProperty.SetValue(true, _headerButton, _yearGrid);
IsVisibleProperty.SetValue(false, _yearButton, _monthButton, _monthGrid, _fastPreviousButton,
_fastNextButton);
_headerButton?.SetValue(ContentControl.ContentProperty,
ContextDate.StartYear + "-" + ContextDate.EndYear);
}
else if (Mode == CalendarViewMode.Decade)
{
IsVisibleProperty.SetValue(true, _headerButton, _yearGrid);
IsVisibleProperty.SetValue(false, _yearButton, _monthButton, _monthGrid, _fastPreviousButton,
_fastNextButton);
_headerButton?.SetValue(ContentControl.ContentProperty,
ContextDate.StartYear + "-" + ContextDate.EndYear);
}
else if (Mode == CalendarViewMode.Year)
{
IsVisibleProperty.SetValue(true, _headerButton, _yearGrid);
IsVisibleProperty.SetValue(false, _yearButton, _monthButton, _monthGrid, _fastPreviousButton,
_fastNextButton);
_headerButton?.SetValue(ContentControl.ContentProperty, ContextDate.Year);
}
else if (Mode == CalendarViewMode.Month)
{
IsVisibleProperty.SetValue(false, _headerButton, _yearGrid);
IsVisibleProperty.SetValue(true, _yearButton, _monthButton, _monthGrid, _fastPreviousButton,
_fastNextButton);
// _headerButton?.SetValue(ContentControl.ContentProperty, ContextCalendar.Year);
_yearButton?.SetValue(ContentControl.ContentProperty, ContextDate.Year);
_monthButton?.SetValue(ContentControl.ContentProperty,
DateTimeHelper.GetCurrentDateTimeFormatInfo().AbbreviatedMonthNames[ContextDate.Month-1 ?? 0]);
}
bool canForward = !(ContextDate.EndYear <= 0) && !(ContextDate.Year <= 0);
bool canNext = !(ContextDate.StartYear > 9999) && !(ContextDate.EndYear > 9999);
IsEnabledProperty.SetValue(canForward, _previousButton, _fastPreviousButton);
IsEnabledProperty.SetValue(canNext, _nextButton, _fastNextButton);
}
public void MarkDates(DateTime? startDate = null, DateTime? endDate = null, DateTime? previewStartDate = null, DateTime? previewEndDate = null)
{
_start = startDate;
_end = endDate;
_previewStart = previewStartDate;
_previewEnd = previewEndDate;
if (_monthGrid?.Children is null) return;
DateTime start = startDate ?? DateTime.MaxValue;
DateTime end = endDate ?? DateTime.MinValue;
DateTime previewStart = previewStartDate ?? DateTime.MaxValue;
DateTime previewEnd = previewEndDate ?? DateTime.MinValue;
DateTime rangeStart = DateTimeHelper.Min(start, previewStart);
DateTime rangeEnd = DateTimeHelper.Max(end, previewEnd);
foreach (var child in _monthGrid.Children)
{
if (child is not CalendarDayButton { DataContext: DateTime d } button) continue;
button.ResetSelection();
if(d.Month != ContextDate.Month) continue;
if (d < rangeEnd && d > rangeStart) button.IsInRange = true;
if (d == previewStart) button.IsPreviewStartDate = true;
if (d == previewEnd) button.IsPreviewEndDate = true;
if (d == startDate) button.IsStartDate = true;
if (d == endDate) button.IsEndDate = true;
if (d == startDate && d == endDate) button.IsSelected = true;
}
}
public void ClearSelection(bool start = true, bool end = true)
{
if (start)
{
_previewStart = null;
_start = null;
}
if (end)
{
_previewEnd = null;
_end = null;
}
if (_monthGrid?.Children is null) return;
foreach (var child in _monthGrid.Children)
{
if (child is not CalendarDayButton button) continue;
if (start)
{
button.IsPreviewStartDate = false;
button.IsStartDate = false;
}
if (end)
{
button.IsEndDate = false;
button.IsInRange = false;
}
button.IsPreviewEndDate = false;
}
UpdateDayButtons();
}
protected override void OnPointerExited(PointerEventArgs e)
{
base.OnPointerExited(e);
DatePreviewed?.Invoke(this, new CalendarDayButtonEventArgs(null));
}
private bool _dateContextSyncing = false;
/// <summary>
/// Used for syncing the context date for DateRangePicker. mark a flag to avoid infinitely loop.
/// </summary>
/// <param name="context"></param>
internal void SyncContextDate(CalendarContext? context)
{
if (context is null) return;
_dateContextSyncing = true;
ContextDate = context;
_dateContextSyncing = false;
UpdateDayButtons();
UpdateYearButtons();
}
}

View File

@@ -0,0 +1,13 @@
namespace Ursa.Controls;
internal enum CalendarViewMode
{
// Show days in current month.
Month,
// Show Months in current year.
Year,
// The button represents 1 years.
Decade,
// The button represents 10 years.
Century,
}

View File

@@ -0,0 +1,68 @@
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Input;
using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Common;
namespace Ursa.Controls;
[PseudoClasses(PC_Range, PseudoClassName.PC_Selected)]
public class CalendarYearButton : ContentControl
{
public const string PC_Range = ":range";
public static readonly RoutedEvent<CalendarYearButtonEventArgs> ItemSelectedEvent =
RoutedEvent.Register<CalendarYearButton, CalendarYearButtonEventArgs>(
nameof(ItemSelected), RoutingStrategies.Bubble);
static CalendarYearButton()
{
PressedMixin.Attach<CalendarYearButton>();
}
internal CalendarContext CalendarContext { get; set; } = new ();
internal CalendarViewMode Mode { get; private set; }
public event EventHandler<CalendarDayButtonEventArgs> ItemSelected
{
add => AddHandler(ItemSelectedEvent, value);
remove => RemoveHandler(ItemSelectedEvent, value);
}
internal void SetContext(CalendarViewMode mode, CalendarContext context)
{
CalendarContext = context.Clone();
Mode = mode;
switch (Mode)
{
case CalendarViewMode.Year:
Content = DateTimeHelper.GetCurrentDateTimeFormatInfo()
.AbbreviatedMonthNames[(CalendarContext.Month - 1) ?? 0];
break;
case CalendarViewMode.Decade:
Content = CalendarContext.Year <= 0 || CalendarContext.Year > 9999
? null
: CalendarContext.Year?.ToString();
break;
case CalendarViewMode.Century:
Content = CalendarContext.EndYear <= 0 || CalendarContext.StartYear > 9999
? null
: CalendarContext.StartYear + "-" + CalendarContext.EndYear;
break;
default:
Content = null;
break;
}
IsEnabled = Content != null;
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
RaiseEvent(new CalendarYearButtonEventArgs(Mode, this.CalendarContext.Clone())
{ RoutedEvent = ItemSelectedEvent, Source = this });
}
}

View File

@@ -0,0 +1,16 @@
using Avalonia.Interactivity;
namespace Ursa.Controls;
public class CalendarYearButtonEventArgs: RoutedEventArgs
{
internal CalendarContext Context { get; }
internal CalendarViewMode Mode { get; }
/// <inheritdoc />
internal CalendarYearButtonEventArgs(CalendarViewMode mode, CalendarContext context)
{
Context = context;
Mode = mode;
}
}

View File

@@ -0,0 +1,191 @@
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Contracts;
using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls;
[TemplatePart(PART_Button, typeof(Button))]
[TemplatePart(PART_Popup, typeof(Popup))]
[TemplatePart(PART_TextBox, typeof(TextBox))]
[TemplatePart(PART_Calendar, typeof(CalendarView))]
public class DatePicker: DatePickerBase, IClearControl
{
public const string PART_Button = "PART_Button";
public const string PART_Popup = "PART_Popup";
public const string PART_TextBox = "PART_TextBox";
public const string PART_Calendar = "PART_Calendar";
private Button? _button;
private Popup? _popup;
private TextBox? _textBox;
private CalendarView? _calendar;
public static readonly StyledProperty<DateTime?> SelectedDateProperty = AvaloniaProperty.Register<DatePicker, DateTime?>(
nameof(SelectedDate), defaultBindingMode: BindingMode.TwoWay);
public DateTime? SelectedDate
{
get => GetValue(SelectedDateProperty);
set => SetValue(SelectedDateProperty, value);
}
public static readonly StyledProperty<string?> WatermarkProperty = AvaloniaProperty.Register<DatePicker, string?>(
nameof(Watermark));
public string? Watermark
{
get => GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
static DatePicker()
{
SelectedDateProperty.Changed.AddClassHandler<DatePicker, DateTime?>((picker, args) =>
picker.OnSelectionChanged(args));
}
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<DateTime?> args)
{
if (args.NewValue.Value is null)
{
_calendar?.ClearSelection();
_textBox?.Clear();
}
else
{
_calendar?.MarkDates(startDate: args.NewValue.Value, endDate: args.NewValue.Value);
_textBox?.SetValue(TextBox.TextProperty, args.NewValue.Value.Value.ToString(DisplayFormat ?? "yyyy-MM-dd"));
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
GotFocusEvent.RemoveHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.RemoveHandler(OnTextChanged, _textBox);
PointerPressedEvent.RemoveHandler(OnTextBoxPointerPressed, _textBox);
Button.ClickEvent.RemoveHandler(OnButtonClick, _button);
if (_calendar != null)
{
_calendar.DateSelected -= OnDateSelected;
}
_button = e.NameScope.Find<Button>(PART_Button);
_popup = e.NameScope.Find<Popup>(PART_Popup);
_textBox = e.NameScope.Find<TextBox>(PART_TextBox);
_calendar = e.NameScope.Find<CalendarView>(PART_Calendar);
Button.ClickEvent.AddHandler(OnButtonClick, RoutingStrategies.Tunnel, true, _button);
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.AddHandler(OnTextChanged, _textBox);
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox);
if (_calendar != null)
{
_calendar.DateSelected += OnDateSelected;
}
}
private void OnDateSelected(object sender, CalendarDayButtonEventArgs e)
{
SetCurrentValue(SelectedDateProperty, e.Date);
SetCurrentValue(IsDropdownOpenProperty, false);
}
private void OnButtonClick(object sender, RoutedEventArgs e)
{
Focus(NavigationMethod.Pointer);
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
}
private void OnTextBoxPointerPressed(object sender, PointerPressedEventArgs e)
{
if (_calendar is not null)
{
var date = SelectedDate ?? DateTime.Today;
_calendar.ContextDate = new CalendarContext(date.Year, date.Month);
_calendar.UpdateDayButtons();
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrEmpty(_textBox?.Text))
{
SetCurrentValue(SelectedDateProperty, null);
_calendar?.ClearSelection();
}
else if (DisplayFormat is null || DisplayFormat.Length == 0)
{
if (DateTime.TryParse(_textBox?.Text, out var defaultTime))
{
SetCurrentValue(SelectedDateProperty, defaultTime);
_calendar?.MarkDates(startDate: defaultTime, endDate: defaultTime);
}
}
else
{
if (DateTime.TryParseExact(_textBox?.Text, DisplayFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None,
out var date))
{
SetCurrentValue(SelectedDateProperty, date);
if (_calendar is not null)
{
var d = SelectedDate ?? DateTime.Today;
_calendar.ContextDate = _calendar.ContextDate.With(year: date.Year, month: date.Month);
_calendar.UpdateDayButtons();
}
_calendar?.MarkDates(startDate: date, endDate: date);
}
}
}
private void OnTextBoxGetFocus(object sender, GotFocusEventArgs e)
{
if (_calendar is not null)
{
var date = SelectedDate ?? DateTime.Today;
_calendar.ContextDate = _calendar.ContextDate.With(year: date.Year, month: date.Month);
_calendar.UpdateDayButtons();
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
SetCurrentValue(IsDropdownOpenProperty, false);
e.Handled = true;
return;
}
if (e.Key == Key.Down)
{
SetCurrentValue(IsDropdownOpenProperty, true);
e.Handled = true;
return;
}
if (e.Key == Key.Tab)
{
SetCurrentValue(IsDropdownOpenProperty, false);
return;
}
base.OnKeyDown(e);
}
public void Clear()
{
}
}

View File

@@ -0,0 +1,115 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Irihi.Avalonia.Shared.Contracts;
namespace Ursa.Controls;
public class DatePickerBase : TemplatedControl, IInnerContentControl, IPopupInnerContent
{
public static readonly StyledProperty<string?> DisplayFormatProperty =
AvaloniaProperty.Register<TimePicker, string?>(
nameof(DisplayFormat), "yyyy-MM-dd");
public static readonly StyledProperty<AvaloniaList<DateRange>> BlackoutDatesProperty =
AvaloniaProperty.Register<DatePickerBase, AvaloniaList<DateRange>>(nameof(BlackoutDates));
public static readonly StyledProperty<IDateSelector?> BlackoutDateRuleProperty =
AvaloniaProperty.Register<DatePickerBase, IDateSelector?>(nameof(BlackoutDateRule));
public static readonly StyledProperty<DayOfWeek> FirstDayOfWeekProperty =
AvaloniaProperty.Register<DatePickerBase, DayOfWeek>(
nameof(FirstDayOfWeek), DateTimeHelper.GetCurrentDateTimeFormatInfo().FirstDayOfWeek);
public static readonly StyledProperty<bool> IsTodayHighlightedProperty =
AvaloniaProperty.Register<DatePickerBase, bool>(nameof(IsTodayHighlighted), true);
public static readonly StyledProperty<object?> InnerLeftContentProperty =
AvaloniaProperty.Register<DatePickerBase, object?>(
nameof(InnerLeftContent));
public static readonly StyledProperty<object?> InnerRightContentProperty =
AvaloniaProperty.Register<DatePickerBase, object?>(
nameof(InnerRightContent));
public static readonly StyledProperty<object?> PopupInnerTopContentProperty =
AvaloniaProperty.Register<DatePickerBase, object?>(
nameof(PopupInnerTopContent));
public static readonly StyledProperty<object?> PopupInnerBottomContentProperty =
AvaloniaProperty.Register<DatePickerBase, object?>(
nameof(PopupInnerBottomContent));
public static readonly StyledProperty<bool> IsDropdownOpenProperty = AvaloniaProperty.Register<DatePickerBase, bool>(
nameof(IsDropdownOpen), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<bool> IsReadonlyProperty = AvaloniaProperty.Register<DatePickerBase, bool>(
nameof(IsReadonly));
public AvaloniaList<DateRange> BlackoutDates
{
get => GetValue(BlackoutDatesProperty);
set => SetValue(BlackoutDatesProperty, value);
}
public IDateSelector? BlackoutDateRule
{
get => GetValue(BlackoutDateRuleProperty);
set => SetValue(BlackoutDateRuleProperty, value);
}
public DayOfWeek FirstDayOfWeek
{
get => GetValue(FirstDayOfWeekProperty);
set => SetValue(FirstDayOfWeekProperty, value);
}
public bool IsTodayHighlighted
{
get => GetValue(IsTodayHighlightedProperty);
set => SetValue(IsTodayHighlightedProperty, value);
}
public bool IsReadonly
{
get => GetValue(IsReadonlyProperty);
set => SetValue(IsReadonlyProperty, value);
}
public bool IsDropdownOpen
{
get => GetValue(IsDropdownOpenProperty);
set => SetValue(IsDropdownOpenProperty, value);
}
public object? InnerLeftContent
{
get => GetValue(InnerLeftContentProperty);
set => SetValue(InnerLeftContentProperty, value);
}
public object? InnerRightContent
{
get => GetValue(InnerRightContentProperty);
set => SetValue(InnerRightContentProperty, value);
}
public object? PopupInnerTopContent
{
get => GetValue(PopupInnerTopContentProperty);
set => SetValue(PopupInnerTopContentProperty, value);
}
public object? PopupInnerBottomContent
{
get => GetValue(PopupInnerBottomContentProperty);
set => SetValue(PopupInnerBottomContentProperty, value);
}
public string? DisplayFormat
{
get => GetValue(DisplayFormatProperty);
set => SetValue(DisplayFormatProperty, value);
}
}

View File

@@ -0,0 +1,45 @@
namespace Ursa.Controls;
/// <summary>
/// Represents a date range. It can be a single day or a range of days. The range is inclusive.
/// </summary>
public sealed record DateRange
{
public DateRange(DateTime day)
{
Start = day.Date;
End = day.Date;
}
public DateRange(DateTime start, DateTime end)
{
if (DateTime.Compare(end, start) >= 0)
{
Start = start.Date;
End = end.Date;
}
else
{
Start = start.Date;
End = start.Date;
}
}
public DateTime Start { get; private set; }
public DateTime End { get; private set; }
public bool Contains(DateTime? date)
{
if (date is null) return false;
return date >= Start && date <= End;
}
}
internal static class DateRangeExtension
{
public static bool Contains(this IEnumerable<DateRange>? ranges, DateTime? date)
{
if (date is null || ranges is null) return false;
return ranges.Any(range => range.Contains(date));
}
}

View File

@@ -0,0 +1,370 @@
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls;
[TemplatePart(PART_Button, typeof(Button))]
[TemplatePart(PART_Popup, typeof(Popup))]
[TemplatePart(PART_StartCalendar, typeof(CalendarView))]
[TemplatePart(PART_EndCalendar, typeof(CalendarView))]
[TemplatePart(PART_StartTextBox, typeof(TextBox))]
[TemplatePart(PART_EndTextBox, typeof(TextBox))]
public class DateRangePicker : DatePickerBase
{
public const string PART_Button = "PART_Button";
public const string PART_Popup = "PART_Popup";
public const string PART_StartCalendar = "PART_StartCalendar";
public const string PART_EndCalendar = "PART_EndCalendar";
public const string PART_StartTextBox = "PART_StartTextBox";
public const string PART_EndTextBox = "PART_EndTextBox";
public static readonly StyledProperty<DateTime?> SelectedStartDateProperty =
AvaloniaProperty.Register<DateRangePicker, DateTime?>(
nameof(SelectedStartDate), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<DateTime?> SelectedEndDateProperty =
AvaloniaProperty.Register<DateRangePicker, DateTime?>(
nameof(SelectedEndDate), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<bool> EnableMonthSyncProperty = AvaloniaProperty.Register<DateRangePicker, bool>(
nameof(EnableMonthSync));
public bool EnableMonthSync
{
get => GetValue(EnableMonthSyncProperty);
set => SetValue(EnableMonthSyncProperty, value);
}
private Button? _button;
private CalendarView? _endCalendar;
private TextBox? _endTextBox;
private Popup? _popup;
private CalendarView? _startCalendar;
private TextBox? _startTextBox;
static DateRangePicker()
{
SelectedStartDateProperty.Changed.AddClassHandler<DateRangePicker, DateTime?>((picker, args) =>
picker.OnSelectionChanged(args));
SelectedEndDateProperty.Changed.AddClassHandler<DateRangePicker, DateTime?>((picker, args) =>
picker.OnSelectionChanged(args));
}
public DateTime? SelectedStartDate
{
get => GetValue(SelectedStartDateProperty);
set => SetValue(SelectedStartDateProperty, value);
}
public DateTime? SelectedEndDate
{
get => GetValue(SelectedEndDateProperty);
set => SetValue(SelectedEndDateProperty, value);
}
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<DateTime?> args)
{
if (args.Property == SelectedStartDateProperty)
{
if (args.NewValue.Value is null)
{
_startCalendar?.ClearSelection();
_startTextBox?.Clear();
}
else
{
_startCalendar?.MarkDates(args.NewValue.Value, args.NewValue.Value);
_startTextBox?.SetValue(TextBox.TextProperty,
args.NewValue.Value.Value.ToString(DisplayFormat ?? "yyyy-MM-dd"));
}
}
else if (args.Property == SelectedEndDateProperty)
{
if (args.NewValue.Value is null)
{
_endCalendar?.ClearSelection();
_endTextBox?.Clear();
}
else
{
_endCalendar?.MarkDates(args.NewValue.Value, args.NewValue.Value);
_endTextBox?.SetValue(TextBox.TextProperty,
args.NewValue.Value.Value.ToString(DisplayFormat ?? "yyyy-MM-dd"));
}
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
GotFocusEvent.RemoveHandler(OnTextBoxGetFocus, _startTextBox);
TextBox.TextChangedEvent.RemoveHandler(OnTextChanged, _startTextBox, _endTextBox);
PointerPressedEvent.RemoveHandler(OnTextBoxPointerPressed, _startTextBox, _endTextBox);
Button.ClickEvent.RemoveHandler(OnButtonClick, _button);
if (_startCalendar != null)
{
_startCalendar.DateSelected -= OnDateSelected;
_startCalendar.DatePreviewed -= OnDatePreviewed;
_startCalendar.ContextDateChanged -= OnContextDateChanged;
}
if (_endCalendar != null)
{
_endCalendar.DateSelected -= OnDateSelected;
_endCalendar.DatePreviewed -= OnDatePreviewed;
_endCalendar.ContextDateChanged -= OnContextDateChanged;
}
_button = e.NameScope.Find<Button>(PART_Button);
_popup = e.NameScope.Find<Popup>(PART_Popup);
_startCalendar = e.NameScope.Find<CalendarView>(PART_StartCalendar);
_endCalendar = e.NameScope.Find<CalendarView>(PART_EndCalendar);
_startTextBox = e.NameScope.Find<TextBox>(PART_StartTextBox);
_endTextBox = e.NameScope.Find<TextBox>(PART_EndTextBox);
Button.ClickEvent.AddHandler(OnButtonClick, RoutingStrategies.Tunnel, true, _button);
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _startTextBox, _endTextBox);
TextBox.TextChangedEvent.AddHandler(OnTextChanged, _startTextBox, _endTextBox);
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _startTextBox, _endTextBox);
if (_startCalendar != null)
{
_startCalendar.DateSelected += OnDateSelected;
_startCalendar.DatePreviewed += OnDatePreviewed;
_startCalendar.ContextDateChanged += OnContextDateChanged;
}
if (_endCalendar != null)
{
_endCalendar.DateSelected += OnDateSelected;
_endCalendar.DatePreviewed += OnDatePreviewed;
_endCalendar.ContextDateChanged += OnContextDateChanged;
}
}
private void OnContextDateChanged(object sender, CalendarContext e)
{
if(sender == _startCalendar && _startCalendar?.Mode == CalendarViewMode.Month)
{
bool needsUpdate = EnableMonthSync || _startCalendar?.ContextDate.CompareTo(_endCalendar?.ContextDate) >= 0;
if (needsUpdate)
{
_endCalendar?.SyncContextDate(_startCalendar?.ContextDate.NextMonth());
}
}
else if(sender == _endCalendar && _endCalendar?.Mode == CalendarViewMode.Month)
{
bool needsUpdate = EnableMonthSync || _endCalendar?.ContextDate.CompareTo(_startCalendar?.ContextDate) <= 0;
if (needsUpdate)
{
_startCalendar?.SyncContextDate(_endCalendar?.ContextDate.PreviousMonth());
}
}
}
private DateTime? _previewStart;
private DateTime? _previewEnd;
private bool? _start;
private void OnDatePreviewed(object sender, CalendarDayButtonEventArgs e)
{
if (_start == true)
{
_previewStart = e.Date;
_startCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
_endCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
}
else if(_start == false)
{
_previewEnd = e.Date;
_startCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
_endCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
}
}
private void OnDateSelected(object sender, CalendarDayButtonEventArgs e)
{
if (_start == true)
{
if (SelectedEndDate < e.Date)
{
SelectedEndDate = null;
}
SetCurrentValue(SelectedStartDateProperty, e.Date);
_startTextBox?.SetValue(TextBox.TextProperty, e.Date?.ToString(DisplayFormat ?? "yyyy-MM-dd"));
_start = false;
_previewStart = null;
_previewEnd = null;
_startCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
_endCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
_endTextBox?.Focus();
}
else if(_start == false)
{
if (SelectedStartDate > e.Date)
{
SelectedStartDate = null;
}
SetCurrentValue(SelectedEndDateProperty, e.Date);
_endTextBox?.SetValue(TextBox.TextProperty, e.Date?.ToString(DisplayFormat ?? "yyyy-MM-dd"));
_start = null;
_previewStart = null;
_previewEnd = null;
_startCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
_endCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
if (SelectedStartDate is null)
{
_start = true;
_startTextBox?.Focus();
}
else
{
SetCurrentValue(IsDropdownOpenProperty, false);
}
}
}
private void OnButtonClick(object sender, RoutedEventArgs e)
{
Focus(NavigationMethod.Pointer);
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
_start = true;
}
private void OnTextBoxPointerPressed(object sender, PointerPressedEventArgs e)
{
if (sender == _startTextBox)
{
_start = true;
if (_startCalendar is not null)
{
var date = SelectedStartDate ?? DateTime.Today;
_startCalendar.ContextDate = new CalendarContext(date.Year, date.Month);
_startCalendar.UpdateDayButtons();
}
if (_endCalendar is not null)
{
var date2 = SelectedEndDate;
if (date2 is null || (date2.Value.Year==SelectedStartDate?.Year && date2.Value.Month == SelectedStartDate?.Month))
{
date2 = SelectedStartDate ?? DateTime.Today;
date2 = date2.Value.AddMonths(1);
}
_endCalendar.ContextDate = new CalendarContext(date2?.Year, date2?.Month);
_endCalendar.UpdateDayButtons();
}
}
else if (sender == _endTextBox)
{
_start = false;
if (_endCalendar is not null)
{
var date = SelectedEndDate ?? DateTime.Today;
_endCalendar.ContextDate = new CalendarContext(date.Year, date.Month);
_endCalendar.UpdateDayButtons();
}
if (_startCalendar is not null)
{
var date2 = SelectedStartDate;
if (date2 is null || (date2.Value.Year==SelectedEndDate?.Year && date2.Value.Month == SelectedEndDate?.Month))
{
date2 = SelectedStartDate ?? DateTime.Today;
date2 = date2.Value.AddMonths(-1);
}
_startCalendar.ContextDate = new CalendarContext(date2?.Year, date2?.Month);
_startCalendar.UpdateDayButtons();
}
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (sender == _startTextBox)
{
OnTextChangedInternal(_startTextBox, SelectedStartDateProperty);
}
else if (sender == _endTextBox)
{
OnTextChangedInternal(_endTextBox, SelectedEndDateProperty);
}
}
private void OnTextChangedInternal(TextBox? textBox, AvaloniaProperty property)
{
if (string.IsNullOrEmpty(textBox?.Text))
{
SetCurrentValue(property, null);
_startCalendar?.ClearSelection(start: true);
_endCalendar?.ClearSelection(end: true);
}
else if (DisplayFormat is null || DisplayFormat.Length == 0)
{
if (DateTime.TryParse(textBox?.Text, out var defaultTime))
{
SetCurrentValue(property, defaultTime);
_startCalendar?.MarkDates(startDate: defaultTime, endDate: defaultTime);
_endCalendar?.MarkDates(startDate: defaultTime, endDate: defaultTime);
}
}
else
{
if (DateTime.TryParseExact(textBox?.Text, DisplayFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None,
out var date))
{
SetCurrentValue(property, date);
if (_startCalendar is not null)
{
var date1 = SelectedStartDate ?? DateTime.Today;
_startCalendar.ContextDate = new CalendarContext(date1.Year, date.Month);
//_startCalendar.SyncContextDate(new CalendarContext(date1.Year, date1.Month));
_startCalendar.UpdateDayButtons();
_startCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
}
if (_endCalendar is not null)
{
var date2 = SelectedEndDate ?? SelectedStartDate ?? DateTime.Today;
if (SelectedEndDate is null) date2 = date2.AddMonths(1);
_endCalendar.ContextDate = new CalendarContext(date2.Year, date2.Month);
//_endCalendar.SyncContextDate(new CalendarContext(date2.Year, date2.Month));
_endCalendar.UpdateDayButtons();
_endCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
}
}
}
}
private void OnTextBoxGetFocus(object sender, GotFocusEventArgs e)
{
if (_startCalendar is not null && _startCalendar?.Mode == CalendarViewMode.Month)
{
var date = SelectedStartDate ?? DateTime.Today;
//_startCalendar.ContextDate = new CalendarContext(date.Year, date.Month);
//_startCalendar.UpdateDayButtons();
}
if (_endCalendar is not null && _endCalendar?.Mode == CalendarViewMode.Month)
{
var date2 = SelectedStartDate ?? DateTime.Today;
date2 = date2.AddMonths(1);
//_endCalendar.ContextDate = new CalendarContext(date2.Year, date2.Month);
//_endCalendar.UpdateDayButtons();
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
//base.OnLostFocus(e);
//SetCurrentValue(IsDropdownOpenProperty, false);
//_start = null;
}
}

View File

@@ -0,0 +1,57 @@
using System.Globalization;
namespace Ursa.Controls;
internal static class DateTimeHelper
{
public const int NumberOfDaysPerWeek = 7;
public const int NumberOfWeeksPerMonth = 6;
public static DateTimeFormatInfo GetCurrentDateTimeFormatInfo()
{
if (CultureInfo.CurrentCulture.Calendar is GregorianCalendar) return CultureInfo.CurrentCulture.DateTimeFormat;
System.Globalization.Calendar? calendar =
CultureInfo.CurrentCulture.OptionalCalendars.OfType<GregorianCalendar>().FirstOrDefault();
string cultureName = calendar is null ? CultureInfo.InvariantCulture.Name : CultureInfo.CurrentCulture.Name;
var dt = new CultureInfo(cultureName).DateTimeFormat;
dt.Calendar = calendar ?? new GregorianCalendar();
return dt;
}
public static DateTime GetFirstDayOfMonth(this DateTime date)
{
return new DateTime(date.Year, date.Month, 1);
}
public static DateTime GetLastDayOfMonth(this DateTime date)
{
return new DateTime(date.Year, date.Month, DateTime.DaysInMonth(date.Year, date.Month));
}
public static int CompareYearMonth(DateTime dt1, DateTime dt2)
{
return (dt1.Year - dt2.Year) * 12 + dt1.Month - dt2.Month;
}
public static DateTime Min(DateTime d1, DateTime d2)
{
return d1.Ticks > d2.Ticks ? d2 : d1;
}
public static DateTime Max(DateTime d1, DateTime d2)
{
return d1.Ticks < d2.Ticks ? d2 : d1;
}
public static (int start, int end) GetDecadeViewRangeByYear(int year)
{
int start = year / 10 * 10;
return new ValueTuple<int, int>(start, start + 10);
}
public static (int start, int end) GetCenturyViewRangeByYear(int year)
{
int start = year / 100 * 100;
return new ValueTuple<int, int>(start, start + 100);
}
}

View File

@@ -0,0 +1,17 @@
namespace Ursa.Controls;
public interface IDateSelector
{
public bool Match(DateTime? date);
}
public class WeekendDateSelector: IDateSelector
{
public static WeekendDateSelector Instance { get; } = new WeekendDateSelector();
public bool Match(DateTime? date)
{
if (date is null) return false;
return date.Value.DayOfWeek == DayOfWeek.Saturday || date.Value.DayOfWeek == DayOfWeek.Sunday;
}
}

View File

@@ -17,7 +17,7 @@
<ItemGroup>
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)"/>
<PackageReference Include="Irihi.Avalonia.Shared" Version="0.1.7" />
<PackageReference Include="Irihi.Avalonia.Shared" Version="0.1.8" />
</ItemGroup>
<ItemGroup>