Merge pull request #397 from irihitech/date

Introduce DateTimePicker
This commit is contained in:
Dong Bin
2024-09-11 21:27:43 +08:00
committed by GitHub
12 changed files with 496 additions and 15 deletions

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="1"
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:DateTimePicker].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 DateTimePickerGlyph}"
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:DateTimePicker].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

@@ -0,0 +1,5 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- Add Resources Here -->
<StreamGeometry x:Key="DateTimePickerGlyph">M2 5V19C2 20.6569 3.34315 22 5 22H12.101C11.5151 21.4259 11.0297 20.7496 10.6736 20H6C4.89543 20 4 19.1046 4 18V8C4 7.44772 4.44772 7 5 7H19C19.5523 7 20 7.44772 20 8V10.6736C20.7496 11.0297 21.4259 11.5151 22 12.101V5C22 3.34315 20.6569 2 19 2H5C3.34315 2 2 3.34315 2 5Z, M16 10H17C16.4614 10 15.9369 10.0608 15.4332 10.176C15.5943 10.065 15.7896 10 16 10Z, M13.4981 10.9376C13.4659 10.4144 13.0313 10 12.5 10H11.5C10.9477 10 10.5 10.4477 10.5 11V12C10.5 12.4742 10.83 12.8712 11.2729 12.9741C11.857 12.1446 12.6168 11.4478 13.4981 10.9376Z, M7 10C6.44772 10 6 10.4477 6 11V12C6 12.5523 6.44772 13 7 13H8C8.55228 13 9 12.5523 9 12V11C9 10.4477 8.55228 10 8 10H7Z, M6 16C6 15.4477 6.44772 15 7 15H8C8.55228 15 9 15.4477 9 16V17C9 17.5523 8.55228 18 8 18H7C6.44772 18 6 17.5523 6 17V16Z, M22 17C22 19.7614 19.7614 22 17 22C14.2386 22 12 19.7614 12 17C12 14.2386 14.2386 12 17 12C19.7614 12 22 14.2386 22 17ZM18 15C18 14.4477 17.5523 14 17 14C16.4477 14 16 14.4477 16 15V17C16 17.2652 16.1054 17.5196 16.2929 17.7071L17.7929 19.2071C18.1834 19.5976 18.8166 19.5976 19.2071 19.2071C19.5976 18.8166 19.5976 18.1834 19.2071 17.7929L18 16.5858V15Z</StreamGeometry>
</ResourceDictionary>

View File

@@ -5,6 +5,7 @@
<MergeResourceInclude Source="Banner.axaml" />
<MergeResourceInclude Source="ButtonGroup.axaml" />
<MergeResourceInclude Source="DatePicker.axaml" />
<MergeResourceInclude Source="DateTimePicker.axaml" />
<MergeResourceInclude Source="Dialog.axaml" />
<MergeResourceInclude Source="DialogShared.axaml" />
<MergeResourceInclude Source="Divider.axaml" />

View File

@@ -81,13 +81,10 @@ public class CalendarView : TemplatedControl
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
@@ -118,8 +115,17 @@ public class CalendarView : TemplatedControl
get => GetValue(FirstDayOfWeekProperty);
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 +426,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>

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);
}
}