feat: introducing DateTimePicker.

This commit is contained in:
rabbitism
2024-09-05 22:47:49 +08:00
parent a552244b42
commit e03b659cfe
10 changed files with 473 additions and 12 deletions

View File

@@ -0,0 +1,12 @@
<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.DateTimePickerDemo">
<StackPanel Margin="20" Spacing="">
<u:DateTimePicker Width="300" PanelFormat="hh mm ss tt"/>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Ursa.Demo.Pages;
public partial class DateTimePickerDemo : UserControl
{
public DateTimePickerDemo()
{
InitializeComponent();
}
}

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Class Input", Key = MenuKeys.MenuKeyClassInput },
new() { MenuHeader = "Clock", Key = MenuKeys.MenuKeyClock, Status = "Updated" },
new() { MenuHeader = "Date Picker", Key = MenuKeys.MenuKeyDatePicker, Status = "New"},
new() { MenuHeader = "Date Time Picker", Key = MenuKeys.MenuKeyDateTimePicker, Status = "New"},
new() { MenuHeader = "Dialog", Key = MenuKeys.MenuKeyDialog },
new() { MenuHeader = "Disable Container", Key = MenuKeys.MenuKeyDisableContainer },
new() { MenuHeader = "Divider", Key = MenuKeys.MenuKeyDivider },
@@ -71,6 +72,7 @@ public static class MenuKeys
public const string MenuKeyClassInput = "Class Input";
public const string MenuKeyClock = "Clock";
public const string MenuKeyDatePicker = "DatePicker";
public const string MenuKeyDateTimePicker = "DateTimePicker";
public const string MenuKeyDialog = "Dialog";
public const string MenuKeyDivider = "Divider";
public const string MenuKeyDisableContainer = "DisableContainer";

View File

@@ -0,0 +1,141 @@
<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:DateTimePicker}" TargetType="u:DateTimePicker">
<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:DateTimePicker">
<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">
<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}"
FontSize="{TemplateBinding FontSize}"
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="1"
Padding="0,0,8,0"
IsVisible="{Binding !#ClearButton.IsVisible}"
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}"
BorderThickness="{DynamicResource ComboBoxPopupBorderThickness}"
BoxShadow="{DynamicResource ComboBoxPopupBoxShadow}"
CornerRadius="{DynamicResource CalendarCornerRadius}">
<Grid ColumnDefinitions="*, *">
<u:CalendarView
Name="{x:Static u:DateTimePicker.PART_Calendar}"
BorderThickness="0"
CornerRadius="{Binding $parent[Border].CornerRadius}"
FirstDayOfWeek="{TemplateBinding FirstDayOfWeek}"
FontSize="{DynamicResource DefaultFontSize}"
IsTodayHighlighted="{TemplateBinding IsTodayHighlighted}" />
<u:TimePickerPresenter Grid.Column="1"
Name="{x:Static u:DateTimePicker.PART_TimePicker}"
FontSize="{DynamicResource DefaultFontSize}"
NeedsConfirmation="{TemplateBinding NeedConfirmation}"
PanelFormat="{TemplateBinding PanelFormat}"/>
</Grid>
</Border>
</Popup>
</Grid>
</Panel>
</DataValidationErrors>
</ControlTemplate>
</Setter>
<Style Selector="^.clearButton, ^.ClearButton">
<Style Selector="^:pointerover /template/ Button#ClearButton">
<Setter Property="IsVisible" Value="{Binding $parent[u:DatePicker].SelectedDate, Converter={x:Static ObjectConverters.IsNotNull}}" />
</Style>
</Style>
<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

@@ -12,6 +12,7 @@
<ResourceInclude Source="Clock.axaml" />
<ResourceInclude Source="DatePicker.axaml" />
<ResourceInclude Source="DateRangePicker.axaml" />
<ResourceInclude Source="DateTimePicker.axaml" />
<ResourceInclude Source="Dialog.axaml" />
<ResourceInclude Source="DialogShared.axaml" />
<ResourceInclude Source="DisableContainer.axaml" />

View File

@@ -119,7 +119,16 @@ public class CalendarView : TemplatedControl
set => SetValue(FirstDayOfWeekProperty, value);
}
public event EventHandler<CalendarDayButtonEventArgs>? DateSelected;
public static readonly RoutedEvent<CalendarDayButtonEventArgs> DateSelectedEvent =
RoutedEvent.Register<TimePickerPresenter, CalendarDayButtonEventArgs>(
nameof(DateSelected), RoutingStrategies.Bubble);
public event EventHandler<CalendarDayButtonEventArgs> DateSelected
{
add => AddHandler(DateSelectedEvent, value);
remove => RemoveHandler(DateSelectedEvent, value);
}
public event EventHandler<CalendarDayButtonEventArgs>? DatePreviewed;
internal event EventHandler<CalendarContext>? ContextDateChanged;
@@ -420,7 +429,7 @@ public class CalendarView : TemplatedControl
ContextDate = ContextDate.With(year: e.Date.Value.Year, month: e.Date.Value.Month);
UpdateDayButtons();
}
DateSelected?.Invoke(this, e);
RaiseEvent(new CalendarDayButtonEventArgs(e.Date) { RoutedEvent = DateSelectedEvent, Source = this });
}
/// <summary>
@@ -531,6 +540,7 @@ public class CalendarView : TemplatedControl
public void MarkDates(DateTime? startDate = null, DateTime? endDate = null, DateTime? previewStartDate = null, DateTime? previewEndDate = null)
{
Debug.WriteLine( startDate + " " + endDate + " " + previewStartDate + " " + previewEndDate);
_start = startDate;
_end = endDate;
_previewStart = previewStartDate;

View File

@@ -63,10 +63,7 @@ public class DatePicker: DatePickerBase, IClearControl
TextBox.TextChangedEvent.RemoveHandler(OnTextChanged, _textBox);
PointerPressedEvent.RemoveHandler(OnTextBoxPointerPressed, _textBox);
Button.ClickEvent.RemoveHandler(OnButtonClick, _button);
if (_calendar != null)
{
_calendar.DateSelected -= OnDateSelected;
}
CalendarView.DateSelectedEvent.RemoveHandler(OnDateSelected, _calendar);
_button = e.NameScope.Find<Button>(PART_Button);
e.NameScope.Find<Popup>(PART_Popup);
@@ -77,11 +74,7 @@ public class DatePicker: DatePickerBase, IClearControl
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.AddHandler(OnTextChanged, _textBox);
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox);
if (_calendar != null)
{
_calendar.DateSelected += OnDateSelected;
}
CalendarView.DateSelectedEvent.AddHandler(OnDateSelected, RoutingStrategies.Bubble, true, _calendar);
SyncSelectedDateToText(SelectedDate);
}

View File

@@ -0,0 +1,282 @@
using System.Globalization;
using System.Runtime.CompilerServices;
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_TextBox, typeof(TextBox))]
[TemplatePart(PART_Calendar, typeof(CalendarView))]
[TemplatePart(PART_TimePicker, typeof(TimePicker))]
public class DateTimePicker : DatePickerBase
{
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";
public const string PART_TimePicker = "PART_TimePicker";
public static readonly StyledProperty<DateTime?> SelectedDateProperty =
AvaloniaProperty.Register<DateTimePicker, DateTime?>(
nameof(SelectedDate), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<string?> WatermarkProperty =
AvaloniaProperty.Register<DateTimePicker, string?>(
nameof(Watermark));
public static readonly StyledProperty<string> PanelFormatProperty = AvaloniaProperty.Register<TimePicker, string>(
nameof(PanelFormat), "HH mm ss");
public static readonly StyledProperty<bool> NeedConfirmationProperty = AvaloniaProperty.Register<TimePicker, bool>(
nameof(NeedConfirmation));
private Button? _button;
private CalendarView? _calendar;
private TextBox? _textBox;
private TimePickerPresenter? _timePickerPresenter;
static DateTimePicker()
{
DisplayFormatProperty.OverrideDefaultValue<DateTimePicker>(CultureInfo.InvariantCulture.DateTimeFormat.FullDateTimePattern);
SelectedDateProperty.Changed.AddClassHandler<DateTimePicker, DateTime?>((picker, args) =>
picker.OnSelectionChanged(args));
}
public DateTime? SelectedDate
{
get => GetValue(SelectedDateProperty);
set => SetValue(SelectedDateProperty, value);
}
public string? Watermark
{
get => GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
public string PanelFormat
{
get => GetValue(PanelFormatProperty);
set => SetValue(PanelFormatProperty, value);
}
public bool NeedConfirmation
{
get => GetValue(NeedConfirmationProperty);
set => SetValue(NeedConfirmationProperty, value);
}
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<DateTime?> args)
{
SyncSelectedDateToText(args.NewValue.Value);
}
private void SyncSelectedDateToText(DateTime? date)
{
if (date is null)
{
_textBox?.SetValue(TextBox.TextProperty, null);
_calendar?.ClearSelection();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, null);
}
else
{
_textBox?.SetValue(TextBox.TextProperty,
date.Value.ToString(DisplayFormat ?? CultureInfo.InvariantCulture.DateTimeFormat.FullDateTimePattern));
_calendar?.MarkDates(date.Value.Date, date.Value.Date);
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, date.Value.TimeOfDay);
}
}
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);
CalendarView.DateSelectedEvent.RemoveHandler(OnDateSelected, _calendar);
TimePickerPresenter.SelectedTimeChangedEvent.RemoveHandler(OnTimeSelectedChanged, _timePickerPresenter);
_button = e.NameScope.Find<Button>(PART_Button);
e.NameScope.Find<Popup>(PART_Popup);
_textBox = e.NameScope.Find<TextBox>(PART_TextBox);
_calendar = e.NameScope.Find<CalendarView>(PART_Calendar);
_timePickerPresenter = e.NameScope.Find<TimePickerPresenter>(PART_TimePicker);
Button.ClickEvent.AddHandler(OnButtonClick, RoutingStrategies.Bubble, true, _button);
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.AddHandler(OnTextChanged, _textBox);
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox);
CalendarView.DateSelectedEvent.AddHandler(OnDateSelected, RoutingStrategies.Bubble, true, _calendar);
TimePickerPresenter.SelectedTimeChangedEvent.AddHandler(OnTimeSelectedChanged, _timePickerPresenter);
SyncSelectedDateToText(SelectedDate);
}
private void OnDateSelected(object? sender, CalendarDayButtonEventArgs e)
{
if (SelectedDate is null)
{
if (e.Date is null) return;
var date = e.Date.Value;
var time = DateTime.Now.TimeOfDay;
SetCurrentValue(SelectedDateProperty,
new DateTime(date.Year, date.Month, date.Day, time.Hours, time.Minutes, time.Seconds));
}
else
{
var selectedDate = SelectedDate;
if (e.Date is null) return;
var date = e.Date.Value;
SetCurrentValue(SelectedDateProperty,
new DateTime(date.Year, date.Month, date.Day, selectedDate.Value.Hour, selectedDate.Value.Minute,
selectedDate.Value.Second));
}
}
private void OnTimeSelectedChanged(object sender, TimeChangedEventArgs e)
{
if (SelectedDate is null)
{
if (e.NewTime is null) return;
var time = e.NewTime.Value;
var date = DateTime.Today;
SetCurrentValue(SelectedDateProperty,
new DateTime(date.Year, date.Month, date.Day, time.Hours, time.Minutes, time.Seconds));
}
else
{
var selectedDate = SelectedDate;
if (e.NewTime is null) return;
var time = e.NewTime.Value;
SetCurrentValue(SelectedDateProperty,
new DateTime(selectedDate.Value.Year, selectedDate.Value.Month, selectedDate.Value.Day, time.Hours,
time.Minutes,
time.Seconds));
}
}
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.Now;
_calendar.ContextDate = new CalendarContext(date.Year, date.Month);
_calendar.UpdateDayButtons();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, SelectedDate?.TimeOfDay);
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void OnTextChanged(object? sender, TextChangedEventArgs e)
{
SetSelectedDate(true);
}
private void SetSelectedDate(bool fromText = false)
{
if (string.IsNullOrEmpty(_textBox?.Text))
{
SetCurrentValue(SelectedDateProperty, null);
_calendar?.ClearSelection();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, null);
}
else if (DisplayFormat is null || DisplayFormat.Length == 0)
{
if (DateTime.TryParse(_textBox?.Text, out var defaultTime))
{
SetCurrentValue(SelectedDateProperty, defaultTime);
_calendar?.MarkDates(defaultTime.Date, defaultTime.Date);
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, defaultTime.TimeOfDay);
}
}
else
{
if (DateTime.TryParseExact(_textBox?.Text, DisplayFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None,
out var date))
{
SetCurrentValue(SelectedDateProperty, date);
if (_calendar is not null)
{
_calendar.ContextDate = _calendar.ContextDate.With(date.Year, date.Month);
_calendar.UpdateDayButtons();
}
_calendar?.MarkDates(date.Date, date.Date);
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, date.TimeOfDay);
}
else
{
SetCurrentValue(SelectedDateProperty, null);
if (!fromText) _textBox?.SetValue(TextBox.TextProperty, null);
_calendar?.ClearSelection();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, null);
}
}
}
private void OnTextBoxGetFocus(object? sender, GotFocusEventArgs e)
{
if (_calendar is not null)
{
var date = SelectedDate ?? DateTime.Today;
_calendar.ContextDate = _calendar.ContextDate.With(date.Year, date.Month);
_calendar.UpdateDayButtons();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, date.TimeOfDay);
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
// SetCurrentValue(IsDropdownOpenProperty, false);
SetSelectedDate();
}
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()
{
SetCurrentValue(SelectedDateProperty, null);
}
}