Refactor TimePicker

Use the new Hover selection event mechanism
This commit is contained in:
polarboy 2024-09-14 12:52:36 +08:00
parent 79e1c510ed
commit 561aaddcd5
29 changed files with 1247 additions and 1087 deletions

View File

@ -51,8 +51,20 @@ Avalonia 11.1.1 及其以上<br>
PS: AtomUI 目前仅在 Windows 11 平台测试<br>
#### 中文社区
目前我们暂时只创建微信开发者群的交流方式,下面是二维码,有兴趣的同学可以扫码加入:
<img src="./docs/images/wechat.jpg" width="200" height="200"/>
目前我们暂时只创建 QQ 和微信开发者群的交流方式,下面是二维码,有兴趣的同学可以扫码加入:
<table border="0">
<tbody>
<tr>
<td align="center" valign="middle">
<img src="./docs/images/wechat.jpg" width="200" height="200"/>
</td>
<td align="center" valign="middle">
<img src="./docs/images/QQ.jpg" width="200" height="200"/>
</td>
</tr>
</tbody>
<table>
> PS扫码请注明来意比如学习`AtomUI`或者`Avalonia`爱好者

BIN
docs/images/QQ.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

View File

@ -5,20 +5,25 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:desktop="clr-namespace:AtomUI.Demo.Desktop"
xmlns:calendarpresenter="clr-namespace:AtomUI.Controls.CalendarView;assembly=AtomUI.Controls"
xmlns:atom="https://atomui.net"
mc:Ignorable="d">
<desktop:ShowCasePanel>
<desktop:ShowCaseItem
Title="Basic"
Description="Click DatePicker, and then we could select or input a date in panel.">
<atom:DatePicker Watermark="Select date" DefaultDateTime="2029-9-3" IsNeedConfirm="True"/>
<atom:DatePicker Watermark="Select date"/>
</desktop:ShowCaseItem>
<desktop:ShowCaseItem
Title="With Time Picker"
Description="Date selection panel with time selection.">
<atom:DatePicker Watermark="Select date" IsShowTime="True"/>
</desktop:ShowCaseItem>
<desktop:ShowCaseItem
Title="Need Confirm"
Description="DatePicker will automatically determine whether to show a confirm button according to the picker property. You can also set the needConfirm property to determine whether to show a confirm button. When needConfirm is set, the user must click the confirm button to complete the selection. Otherwise, the selection will be submitted when the picker loses focus or selects a date.">
<atom:DatePicker Watermark="Select date" IsNeedConfirm="True"/>
</desktop:ShowCaseItem>
<!-- -->
<!-- <desktop:ShowCaseItem -->
<!-- Title="Basic" -->
<!-- Description="Click DatePicker, and then we could select or input a date in panel."> -->
<!-- <calendarpresenter:Calendar SelectedDate="2008-9-3"/> -->
<!-- </desktop:ShowCaseItem> -->
</desktop:ShowCasePanel>
</UserControl>

View File

@ -11,7 +11,7 @@ using Avalonia.Media;
namespace AtomUI.Controls.CalendarView;
public class DateSelectedEventArgs : RoutedEventArgs
public class DateSelectedEventArgs : EventArgs
{
public DateTime? Value { get; }
public DateSelectedEventArgs(DateTime? value)

View File

@ -4,7 +4,6 @@ using AtomUI.Data;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Threading;
namespace AtomUI.Controls;

View File

@ -98,7 +98,6 @@ internal class DatePickerPresenter : PickerPresenterBase
private Button? _confirmButton;
private PickerCalendar? _calendarView;
private IDisposable? _choosingStateDisposable;
private bool _isConfirmed;
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
@ -112,13 +111,12 @@ internal class DatePickerPresenter : PickerPresenterBase
ChoosingStatueChanged?.Invoke(this, new ChoosingStatusEventArgs(args.GetNewValue<bool>()));
});
}
_isConfirmed = false;
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_choosingStateDisposable?.Dispose();
_choosingStateDisposable = null;
}

View File

@ -18,8 +18,8 @@ internal class DatePickerPresenterTheme : BaseControlTheme
public const string NowButtonPart = "PART_NowButton";
public const string TodayButtonPart = "PART_TodayButton";
public const string ConfirmButtonPart = "PART_ConfirmButton";
public const string ButtonsContainerPart = "PART_ButtonsContainer";
public const string ButtonsContainerFramePart = "PART_ButtonsContainerFrame";
public const string ButtonsLayoutPart = "PART_ButtonsLayout";
public const string ButtonsFramePart = "PART_ButtonsFrame";
public const string CalendarViewPart = "PART_CalendarView";
public DatePickerPresenterTheme() : this(typeof(DatePickerPresenter))
@ -44,7 +44,7 @@ internal class DatePickerPresenterTheme : BaseControlTheme
var buttonsContainerFrame = new Border()
{
Name = ButtonsContainerFramePart
Name = ButtonsFramePart
};
CreateTemplateParentBinding(buttonsContainerFrame, Border.BorderThicknessProperty, DatePickerPresenter.BorderThicknessProperty);
var buttonsPanel = BuildButtons(presenter, scope);
@ -73,7 +73,7 @@ internal class DatePickerPresenterTheme : BaseControlTheme
{
var buttonsPanel = new Panel()
{
Name = ButtonsContainerPart
Name = ButtonsLayoutPart
};
var nowButton = new Button
@ -114,11 +114,11 @@ internal class DatePickerPresenterTheme : BaseControlTheme
protected override void BuildStyles()
{
var buttonsFrameStyle = new Style(selector => selector.Nesting().Template().Name(ButtonsContainerFramePart));
var buttonsFrameStyle = new Style(selector => selector.Nesting().Template().Name(ButtonsFramePart));
buttonsFrameStyle.Add(Border.BorderBrushProperty, GlobalTokenResourceKey.ColorBorderSecondary);
Add(buttonsFrameStyle);
var buttonsPanelStyle = new Style(selector => selector.Nesting().Template().Name(ButtonsContainerPart));
var buttonsPanelStyle = new Style(selector => selector.Nesting().Template().Name(ButtonsLayoutPart));
buttonsPanelStyle.Add(Panel.MarginProperty, DatePickerTokenResourceKey.ButtonsPanelMargin);
Add(buttonsPanelStyle);

View File

@ -25,10 +25,6 @@ internal class DatePickerTheme : InfoPickerInputTheme
base.BuildStyles();
var commonStyle = new Style(selector => selector.Nesting());
commonStyle.Add(DatePicker.MinWidthProperty, DatePickerTokenResourceKey.PickerInputMinWidth);
var choosingStyle = new Style(selector => selector.Nesting().Class(DatePicker.ChoosingPC));
choosingStyle.Add(DatePicker.InputTextBrushProperty, GlobalTokenResourceKey.ColorTextTertiary);
commonStyle.Add(choosingStyle);
Add(commonStyle);
}

View File

@ -89,11 +89,11 @@
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.TabStripItemTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.TabStripTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.TagTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.RangeTimePickerFlyoutPresenterTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.RangeTimePickerTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.TimePickerFlyoutPresenterTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.TimePickerPresenterTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.TimePickerTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.TimeViewTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.ToolTipTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.NodeSwitcherButtonTheme());
ThemeManager.Current.RegisterControlTheme(new AtomUI.Controls.TreeViewItemTheme());

View File

@ -581,6 +581,7 @@ namespace AtomUI.Theme.Styling
public static readonly TokenResourceKey PickerPopupHeight = new TokenResourceKey("TimePicker.PickerPopupHeight", "AtomUI.Token");
public static readonly TokenResourceKey RangePickerArrowMargin = new TokenResourceKey("TimePicker.RangePickerArrowMargin", "AtomUI.Token");
public static readonly TokenResourceKey RangePickerIndicatorThickness = new TokenResourceKey("TimePicker.RangePickerIndicatorThickness", "AtomUI.Token");
public static readonly TokenResourceKey PickerInputMinWidth = new TokenResourceKey("TimePicker.PickerInputMinWidth", "AtomUI.Token");
}
public static class ToolTipTokenResourceKey

View File

@ -140,6 +140,11 @@ internal class InfoPickerInputTheme : BaseControlTheme
base.BuildStyles();
var commonStyle = new Style(selector => selector.Nesting());
commonStyle.Add(InfoPickerInput.InputTextBrushProperty, GlobalTokenResourceKey.ColorText);
var choosingStyle = new Style(selector => selector.Nesting().Class(InfoPickerInput.ChoosingPC));
choosingStyle.Add(InfoPickerInput.InputTextBrushProperty, GlobalTokenResourceKey.ColorTextTertiary);
commonStyle.Add(choosingStyle);
Add(commonStyle);
}
}

View File

@ -1,4 +1,8 @@
using Avalonia;
using AtomUI.Media;
using AtomUI.Theme.Utils;
using Avalonia;
using Avalonia.Animation;
using Avalonia.LogicalTree;
namespace AtomUI.Controls;
@ -33,6 +37,53 @@ public class ListBoxItem : AvaloniaListBoxItem
get => _disabledItemHoverEffect;
set => SetAndRaise(DisabledItemHoverEffectProperty, ref _disabledItemHoverEffect, value);
}
internal static readonly DirectProperty<ListBoxItem, bool> DisabledItemHoverAnimationProperty =
AvaloniaProperty.RegisterDirect<ListBoxItem, bool>(nameof(DisabledItemHoverAnimation),
o => o.DisabledItemHoverAnimation,
(o, v) => o.DisabledItemHoverAnimation = v);
private bool _disabledItemHoverAnimation = false;
internal bool DisabledItemHoverAnimation
{
get => _disabledItemHoverAnimation;
set => SetAndRaise(DisabledItemHoverAnimationProperty, ref _disabledItemHoverAnimation, value);
}
#endregion
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
if (!DisabledItemHoverAnimation)
{
Transitions ??= new Transitions
{
AnimationUtils.CreateTransition<SolidColorBrushTransition>(BackgroundProperty)
};
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (VisualRoot is not null)
{
if (change.Property == DisabledItemHoverAnimationProperty)
{
if (DisabledItemHoverAnimation)
{
Transitions?.Clear();
}
else
{
Transitions = new Transitions
{
AnimationUtils.CreateTransition<SolidColorBrushTransition>(BackgroundProperty)
};
}
}
}
}
}

View File

@ -1,8 +1,5 @@
using AtomUI.Media;
using AtomUI.Theme;
using AtomUI.Theme;
using AtomUI.Theme.Styling;
using AtomUI.Theme.Utils;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
@ -34,11 +31,10 @@ internal class ListBoxItemTheme : BaseControlTheme
Name = ContentPresenterPart
};
contentPresenter.Transitions = new Transitions
{
AnimationUtils.CreateTransition<SolidColorBrushTransition>(ContentPresenter.BackgroundProperty)
};
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ForegroundProperty,
TemplatedControl.ForegroundProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.BackgroundProperty,
TemplatedControl.BackgroundProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.CornerRadiusProperty,
TemplatedControl.CornerRadiusProperty);
CreateTemplateParentBinding(contentPresenter, ContentPresenter.ContentProperty,
@ -66,32 +62,20 @@ internal class ListBoxItemTheme : BaseControlTheme
private void BuildCommonStyle()
{
var commonStyle = new Style(selector => selector.Nesting());
commonStyle.Add(Layoutable.MarginProperty, ListBoxTokenResourceKey.ItemMargin);
{
var contentPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ContentPresenterPart));
contentPresenterStyle.Add(ContentPresenter.ForegroundProperty, ListBoxTokenResourceKey.ItemColor);
contentPresenterStyle.Add(ContentPresenter.BackgroundProperty, ListBoxTokenResourceKey.ItemBgColor);
commonStyle.Add(contentPresenterStyle);
}
commonStyle.Add(ListBoxItem.MarginProperty, ListBoxTokenResourceKey.ItemMargin);
commonStyle.Add(ListBoxItem.ForegroundProperty, ListBoxTokenResourceKey.ItemColor);
commonStyle.Add(ListBoxItem.BackgroundProperty, ListBoxTokenResourceKey.ItemBgColor);
var disabledItemHoverStyle = new Style(selector =>
selector.Nesting().PropertyEquals(ListBoxItem.DisabledItemHoverEffectProperty, false));
{
var contentPresenterStyle = new Style(selector =>
selector.Nesting().Template().Name(ContentPresenterPart).Class(StdPseudoClass.PointerOver));
contentPresenterStyle.Add(ContentPresenter.ForegroundProperty, ListBoxTokenResourceKey.ItemHoverColor);
contentPresenterStyle.Add(ContentPresenter.BackgroundProperty, ListBoxTokenResourceKey.ItemHoverBgColor);
disabledItemHoverStyle.Add(contentPresenterStyle);
}
selector.Nesting().PropertyEquals(ListBoxItem.DisabledItemHoverEffectProperty, false).Class(StdPseudoClass.PointerOver));
disabledItemHoverStyle.Add(ListBoxItem.ForegroundProperty, ListBoxTokenResourceKey.ItemHoverColor);
disabledItemHoverStyle.Add(ListBoxItem.BackgroundProperty, ListBoxTokenResourceKey.ItemHoverBgColor);
commonStyle.Add(disabledItemHoverStyle);
var selectedStyle = new Style(selector => selector.Nesting().Class(StdPseudoClass.Selected));
{
var contentPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ContentPresenterPart));
contentPresenterStyle.Add(ContentPresenter.ForegroundProperty, ListBoxTokenResourceKey.ItemSelectedColor);
contentPresenterStyle.Add(ContentPresenter.BackgroundProperty, ListBoxTokenResourceKey.ItemSelectedBgColor);
selectedStyle.Add(contentPresenterStyle);
}
selectedStyle.Add(ListBoxItem.ForegroundProperty, ListBoxTokenResourceKey.ItemSelectedColor);
selectedStyle.Add(ListBoxItem.BackgroundProperty, ListBoxTokenResourceKey.ItemSelectedBgColor);
commonStyle.Add(selectedStyle);
Add(commonStyle);
}

View File

@ -1,6 +1,5 @@
using System.Diagnostics;
using AtomUI.Controls.TimePickerLang;
using AtomUI.Data;
using AtomUI.Theme.Data;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
@ -29,31 +28,99 @@ public enum DateTimePickerPanelType
TimePeriod //AM or PM
}
public struct CellHoverInfo
{
public DateTimePickerPanelType PanelType { get; }
public int CellValue { get; }
public CellHoverInfo(DateTimePickerPanelType panelType, int cellValue)
{
PanelType = panelType;
CellValue = cellValue;
}
}
internal class CellHoverEventArgs : EventArgs
{
public CellHoverInfo? CellHoverInfo { get; }
public CellHoverEventArgs(CellHoverInfo? hoverInfo)
{
CellHoverInfo = hoverInfo;
}
}
internal class DateTimePickerPanel : Panel, ILogicalScrollable
{
/// <summary>
/// Defines the <see cref="ItemHeight" /> property
/// </summary>
public static readonly StyledProperty<double> ItemHeightProperty =
#region
/// <summary>
/// Defines the <see cref="ItemHeight" /> property
/// </summary>
public static readonly StyledProperty<double> ItemHeightProperty =
AvaloniaProperty.Register<DateTimePickerPanel, double>(nameof(ItemHeight), 40.0);
/// <summary>
/// Defines the <see cref="PanelType" /> property
/// </summary>
public static readonly StyledProperty<DateTimePickerPanelType> PanelTypeProperty =
/// <summary>
/// Defines the <see cref="PanelType" /> property
/// </summary>
public static readonly StyledProperty<DateTimePickerPanelType> PanelTypeProperty =
AvaloniaProperty.Register<DateTimePickerPanel, DateTimePickerPanelType>(nameof(PanelType));
/// <summary>
/// Defines the <see cref="ItemFormat" /> property
/// </summary>
public static readonly StyledProperty<string> ItemFormatProperty =
/// <summary>
/// Defines the <see cref="ItemFormat" /> property
/// </summary>
public static readonly StyledProperty<string> ItemFormatProperty =
AvaloniaProperty.Register<DateTimePickerPanel, string>(nameof(ItemFormat), "yyyy");
/// <summary>
/// Defines the <see cref="ShouldLoop" /> property
/// </summary>
public static readonly StyledProperty<bool> ShouldLoopProperty =
/// <summary>
/// Defines the <see cref="ShouldLoop" /> property
/// </summary>
public static readonly StyledProperty<bool> ShouldLoopProperty =
AvaloniaProperty.Register<DateTimePickerPanel, bool>(nameof(ShouldLoop));
/// <summary>
/// Gets or sets the height of each item
/// </summary>
public double ItemHeight
{
get => GetValue(ItemHeightProperty);
set => SetValue(ItemHeightProperty, value);
}
/// <summary>
/// Gets or sets what this panel displays in date or time units
/// </summary>
public DateTimePickerPanelType PanelType
{
get => GetValue(PanelTypeProperty);
set => SetValue(PanelTypeProperty, value);
}
/// <summary>
/// Gets or sets the string format for the items, using standard
/// .net DateTime or TimeSpan formatting. Format must match panel type
/// </summary>
public string ItemFormat
{
get => GetValue(ItemFormatProperty);
set => SetValue(ItemFormatProperty, value);
}
/// <summary>
/// Gets or sets whether the panel should loop
/// </summary>
public bool ShouldLoop
{
get => GetValue(ShouldLoopProperty);
set => SetValue(ShouldLoopProperty, value);
}
#endregion
#region
internal event EventHandler<CellHoverEventArgs>? CellHovered;
#endregion
//Backing fields for properties
private int _minimumValue = 1;
@ -76,7 +143,7 @@ internal class DateTimePickerPanel : Panel, ILogicalScrollable
public DateTimePickerPanel()
{
FormatDate = DateTime.Now;
AddHandler(TappedEvent, OnItemTapped, RoutingStrategies.Bubble);
AddHandler(TappedEvent, HandleItemTapped, RoutingStrategies.Bubble);
}
static DateTimePickerPanel()
@ -85,44 +152,7 @@ internal class DateTimePickerPanel : Panel, ILogicalScrollable
BackgroundProperty.OverrideDefaultValue<DateTimePickerPanel>(Brushes.Transparent);
AffectsMeasure<DateTimePickerPanel>(ItemHeightProperty);
}
/// <summary>
/// Gets or sets what this panel displays in date or time units
/// </summary>
public DateTimePickerPanelType PanelType
{
get => GetValue(PanelTypeProperty);
set => SetValue(PanelTypeProperty, value);
}
/// <summary>
/// Gets or sets the height of each item
/// </summary>
public double ItemHeight
{
get => GetValue(ItemHeightProperty);
set => SetValue(ItemHeightProperty, value);
}
/// <summary>
/// Gets or sets the string format for the items, using standard
/// .net DateTime or TimeSpan formatting. Format must match panel type
/// </summary>
public string ItemFormat
{
get => GetValue(ItemFormatProperty);
set => SetValue(ItemFormatProperty, value);
}
/// <summary>
/// Gets or sets whether the panel should loop
/// </summary>
public bool ShouldLoop
{
get => GetValue(ShouldLoopProperty);
set => SetValue(ShouldLoopProperty, value);
}
/// <summary>
/// Gets or sets the minimum value
/// </summary>
@ -370,6 +400,7 @@ internal class DateTimePickerPanel : Panel, ILogicalScrollable
{
UpdateItems();
RaiseScrollInvalidated(EventArgs.Empty);
EnableCellHoverAnimation();
_hasInit = true;
}
@ -532,7 +563,16 @@ internal class DateTimePickerPanel : Panel, ILogicalScrollable
HorizontalContentAlignment = HorizontalAlignment.Center,
Focusable = false,
CornerRadius = new CornerRadius(0),
SizeType = SizeType.Middle
SizeType = SizeType.Middle,
DisabledItemHoverAnimation = true
};
item.PointerEntered += (sender, args) =>
{
if (sender is ListBoxItem target)
{
var cellValue = (int)target.Tag!;
CellHovered?.Invoke(this, new CellHoverEventArgs(new CellHoverInfo(PanelType, cellValue)));
}
};
TokenResourceBinder.CreateTokenBinding(item, TemplatedControl.PaddingProperty,
TimePickerTokenResourceKey.ItemPadding, BindingPriority.LocalValue);
@ -640,7 +680,7 @@ internal class DateTimePickerPanel : Panel, ILogicalScrollable
return newValue;
}
private void OnItemTapped(object? sender, TappedEventArgs e)
private void HandleItemTapped(object? sender, TappedEventArgs e)
{
if (e.Source is Visual source &&
GetItemFromSource(source) is ListBoxItem listBoxItem &&
@ -687,4 +727,14 @@ internal class DateTimePickerPanel : Panel, ILogicalScrollable
Offset = Offset.WithY(snapY);
}
}
private void EnableCellHoverAnimation()
{
var children = Children;
for (var i = 0; i < children.Count; i++)
{
var item = (ListBoxItem)children[i];
item.DisabledItemHoverAnimation = false;
}
}
}

View File

@ -104,7 +104,7 @@ public class RangeTimePicker : RangeInfoPickerInput
protected override Flyout CreatePickerFlyout()
{
return new RangeTimePickerFlyout(this);
return new TimePickerFlyout();
}
protected override void NotifyFlyoutAboutToClose(bool selectedIsValid)

View File

@ -1,31 +0,0 @@
using AtomUI.Data;
using Avalonia.Controls;
namespace AtomUI.Controls;
internal class RangeTimePickerFlyout : Flyout
{
internal RangeTimePicker RangeTimePickerRef { get; set; }
public RangeTimePickerFlyout(RangeTimePicker timePicker)
{
RangeTimePickerRef = timePicker;
}
protected override Control CreatePresenter()
{
var presenter = new RangeTimePickerFlyoutPresenter(RangeTimePickerRef);
BindUtils.RelayBind(this, IsShowArrowEffectiveProperty, presenter, IsShowArrowProperty);
BindUtils.RelayBind(RangeTimePickerRef, RangeTimePicker.MinuteIncrementProperty, presenter,
RangeTimePickerFlyoutPresenter.MinuteIncrementProperty);
BindUtils.RelayBind(RangeTimePickerRef, RangeTimePicker.SecondIncrementProperty, presenter,
RangeTimePickerFlyoutPresenter.SecondIncrementProperty);
BindUtils.RelayBind(RangeTimePickerRef, RangeTimePicker.ClockIdentifierProperty, presenter,
RangeTimePickerFlyoutPresenter.ClockIdentifierProperty);
CalculateShowArrowEffective();
SetupArrowPosition(Popup, presenter);
return presenter;
}
}

View File

@ -1,164 +0,0 @@
using System.Reactive.Disposables;
using AtomUI.Controls.Internal;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace AtomUI.Controls;
internal class RangeTimePickerFlyoutPresenter : FlyoutPresenter
{
public static readonly StyledProperty<int> MinuteIncrementProperty =
TimePicker.MinuteIncrementProperty.AddOwner<RangeTimePickerFlyoutPresenter>();
public static readonly StyledProperty<int> SecondIncrementProperty =
TimePicker.SecondIncrementProperty.AddOwner<RangeTimePickerFlyoutPresenter>();
public static readonly StyledProperty<TimeSpan> TimeProperty =
AvaloniaProperty.Register<RangeTimePickerFlyoutPresenter, TimeSpan>(nameof(Time));
public static readonly StyledProperty<ClockIdentifierType> ClockIdentifierProperty =
TimePicker.ClockIdentifierProperty.AddOwner<RangeTimePickerFlyoutPresenter>();
public int MinuteIncrement
{
get => GetValue(MinuteIncrementProperty);
set => SetValue(MinuteIncrementProperty, value);
}
public int SecondIncrement
{
get => GetValue(SecondIncrementProperty);
set => SetValue(SecondIncrementProperty, value);
}
public TimeSpan Time
{
get => GetValue(TimeProperty);
set => SetValue(TimeProperty, value);
}
public ClockIdentifierType ClockIdentifier
{
get => GetValue(ClockIdentifierProperty);
set => SetValue(ClockIdentifierProperty, value);
}
protected override Type StyleKeyOverride => typeof(TimePickerFlyoutPresenter);
internal RangeTimePicker TimePickerRef { get; set; }
private TimePickerPresenter? _timePickerPresenter;
private CompositeDisposable? _compositeDisposable;
private Button? _confirmButton;
private Button? _nowButton;
public RangeTimePickerFlyoutPresenter(RangeTimePicker timePicker)
{
TimePickerRef = timePicker;
HorizontalAlignment = HorizontalAlignment.Left;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_timePickerPresenter = e.NameScope.Get<TimePickerPresenter>(ArrowDecoratedBoxTheme.ContentPresenterPart);
_confirmButton = e.NameScope.Get<Button>(TimePickerFlyoutPresenterTheme.ConfirmButtonPart);
_nowButton = e.NameScope.Get<Button>(TimePickerFlyoutPresenterTheme.NowButtonPart);
if (_timePickerPresenter is not null)
{
_timePickerPresenter.Confirmed += (sender, args) =>
{
TimePickerRef.NotifyConfirmed(_timePickerPresenter.Time);
};
}
if (_confirmButton is not null)
{
_confirmButton.Click += HandleConfirmButtonClicked;
}
if (_nowButton is not null)
{
_nowButton.Click += HandleNowButtonClicked;
}
}
private void SetupTime()
{
if (TimePickerRef.RangeActivatedPart == RangeActivatedPart.Start)
{
if (TimePickerRef.RangeStartSelectedTime is not null)
{
Time = TimePickerRef.RangeStartSelectedTime.Value;
}
}
else if (TimePickerRef.RangeActivatedPart == RangeActivatedPart.End)
{
if (TimePickerRef.RangeEndSelectedTime is not null)
{
Time = TimePickerRef.RangeEndSelectedTime.Value;
}
}
}
private void HandleNowButtonClicked(object? sender, RoutedEventArgs args)
{
_timePickerPresenter?.NowConfirm();
TimePickerRef.ClosePickerFlyout();
}
private void HandleConfirmButtonClicked(object? sender, RoutedEventArgs args)
{
_timePickerPresenter?.Confirm();
if (TimePickerRef.RangeActivatedPart == RangeActivatedPart.Start)
{
if (TimePickerRef.RangeEndSelectedTime is null)
{
TimePickerRef.RangeActivatedPart = RangeActivatedPart.End;
return;
}
}
else if (TimePickerRef.RangeActivatedPart == RangeActivatedPart.End)
{
if (TimePickerRef.RangeStartSelectedTime is null)
{
TimePickerRef.RangeActivatedPart = RangeActivatedPart.Start;
return;
}
}
TimePickerRef.ClosePickerFlyout();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
_compositeDisposable = new CompositeDisposable();
if (_timePickerPresenter is not null)
{
_compositeDisposable.Add(TimePickerPresenter.TemporaryTimeProperty.Changed.Subscribe(args =>
{
TimePickerRef.NotifyTemporaryTimeSelected(args.GetNewValue<TimeSpan>());
}));
}
_compositeDisposable.Add(RangeTimePicker.RangeActivatedPartProperty.Changed.Subscribe(HandleRangeActivatedPartChanged));
SetupTime();
}
private void HandleRangeActivatedPartChanged(AvaloniaPropertyChangedEventArgs args)
{
SetupTime();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_compositeDisposable?.Dispose();
_compositeDisposable = null;
_timePickerPresenter!.Time = TimeSpan.Zero;
}
}

View File

@ -1,93 +0,0 @@
using AtomUI.Controls.Localization;
using AtomUI.Controls.TimePickerLang;
using AtomUI.Theme.Data;
using AtomUI.Theme.Styling;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Styling;
namespace AtomUI.Controls;
[ControlThemeProvider]
internal class RangeTimePickerFlyoutPresenterTheme : ArrowDecoratedBoxTheme
{
public const string ContentLayoutPart = "PART_ContentLayout";
public const string NowButtonPart = "PART_NowButton";
public const string ConfirmButtonPart = "PART_ConfirmButton";
public const string ButtonsContainerPart = "PART_ButtonsContainer";
public RangeTimePickerFlyoutPresenterTheme()
: base(typeof(RangeTimePickerFlyoutPresenter))
{
}
protected override Control BuildContent(INameScope scope)
{
var contentLayout = new DockPanel
{
Name = ContentLayoutPart
};
var contentPresenter = new TimePickerPresenter
{
Name = ContentPresenterPart,
IsShowHeader = false
};
CreateTemplateParentBinding(contentPresenter, TimePickerPresenter.MinuteIncrementProperty,
TimePickerFlyoutPresenter.MinuteIncrementProperty);
CreateTemplateParentBinding(contentPresenter, TimePickerPresenter.SecondIncrementProperty,
TimePickerFlyoutPresenter.SecondIncrementProperty);
CreateTemplateParentBinding(contentPresenter, TimePickerPresenter.ClockIdentifierProperty,
TimePickerFlyoutPresenter.ClockIdentifierProperty);
CreateTemplateParentBinding(contentPresenter, TimePickerPresenter.TimeProperty,
TimePickerFlyoutPresenter.TimeProperty);
contentPresenter.RegisterInNameScope(scope);
var buttons = new Panel
{
Name = ButtonsContainerPart
};
DockPanel.SetDock(buttons, Dock.Bottom);
var nowButton = new Button
{
ButtonType = ButtonType.Link,
Name = NowButtonPart,
HorizontalAlignment = HorizontalAlignment.Left,
SizeType = SizeType.Small
};
LanguageResourceBinder.CreateBinding(nowButton, Button.TextProperty, TimePickerLangResourceKey.Now);
nowButton.RegisterInNameScope(scope);
buttons.Children.Add(nowButton);
var confirmButton = new Button
{
Name = ConfirmButtonPart,
ButtonType = ButtonType.Primary,
SizeType = SizeType.Small,
HorizontalAlignment = HorizontalAlignment.Right
};
LanguageResourceBinder.CreateBinding(confirmButton, Button.TextProperty, CommonLangResourceKey.OkText);
confirmButton.RegisterInNameScope(scope);
buttons.Children.Add(confirmButton);
contentLayout.Children.Add(buttons);
contentLayout.Children.Add(contentPresenter);
return contentLayout;
}
protected override void BuildStyles()
{
base.BuildStyles();
var buttonsContainerStyle = new Style(selector => selector.Nesting().Template().Name(ButtonsContainerPart));
buttonsContainerStyle.Add(Layoutable.MarginProperty, TimePickerTokenResourceKey.ButtonsMargin);
Add(buttonsContainerStyle);
var contentPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ContentPresenterPart));
contentPresenterStyle.Add(Layoutable.MaxWidthProperty, TimePickerTokenResourceKey.PickerPopupWidth);
contentPresenterStyle.Add(Layoutable.HeightProperty, TimePickerTokenResourceKey.PickerPopupHeight);
Add(contentPresenterStyle);
}
}

View File

@ -1,8 +1,9 @@
using AtomUI.Controls.Internal;
using AtomUI.Controls.Utils;
using AtomUI.Data;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
namespace AtomUI.Controls;
@ -16,6 +17,12 @@ public class TimePicker : InfoPickerInput
{
#region
public static readonly StyledProperty<bool> IsNeedConfirmProperty =
AvaloniaProperty.Register<TimePicker, bool>(nameof(IsNeedConfirm));
public static readonly StyledProperty<bool> IsShowNowProperty =
AvaloniaProperty.Register<TimePicker, bool>(nameof(IsShowNow), true);
public static readonly StyledProperty<int> MinuteIncrementProperty =
AvaloniaProperty.Register<TimePicker, int>(nameof(MinuteIncrement), 1, coerce: CoerceMinuteIncrement);
@ -27,13 +34,24 @@ public class TimePicker : InfoPickerInput
public static readonly StyledProperty<TimeSpan?> SelectedTimeProperty =
AvaloniaProperty.Register<TimePicker, TimeSpan?>(nameof(SelectedTime),
defaultBindingMode: BindingMode.TwoWay,
enableDataValidation: true);
public static readonly StyledProperty<TimeSpan?> DefaultTimeProperty =
AvaloniaProperty.Register<TimePicker, TimeSpan?>(nameof(DefaultTime),
enableDataValidation: true);
public bool IsNeedConfirm
{
get => GetValue(IsNeedConfirmProperty);
set => SetValue(IsNeedConfirmProperty, value);
}
public bool IsShowNow
{
get => GetValue(IsShowNowProperty);
set => SetValue(IsShowNowProperty, value);
}
public int MinuteIncrement
{
get => GetValue(MinuteIncrementProperty);
@ -66,38 +84,96 @@ public class TimePicker : InfoPickerInput
#endregion
internal void NotifyTemporaryTimeSelected(TimeSpan value)
{
Text = DateTimeUtils.FormatTimeSpan(value, ClockIdentifier == ClockIdentifierType.HourClock12);
}
internal void NotifyConfirmed(TimeSpan value)
{
_currentValidSelected = true;
SelectedTime = value;
}
private TimePickerPresenter? _pickerPresenter;
protected override Flyout CreatePickerFlyout()
{
return new TimePickerFlyout(this);
return new TimePickerFlyout();
}
protected override void NotifyFlyoutPresenterCreated(Control flyoutPresenter)
{
if (flyoutPresenter is TimePickerFlyoutPresenter timePickerFlyoutPresenter)
{
timePickerFlyoutPresenter.AttachedToVisualTree += (sender, args) =>
{
_pickerPresenter = timePickerFlyoutPresenter.TimePickerPresenter;
ConfigurePickerPresenter(_pickerPresenter);
};
}
}
private void ConfigurePickerPresenter(TimePickerPresenter? presenter)
{
if (presenter is null)
{
return;
}
BindUtils.RelayBind(this, MinuteIncrementProperty, presenter, TimePickerPresenter.MinuteIncrementProperty);
BindUtils.RelayBind(this, SecondIncrementProperty, presenter, TimePickerPresenter.SecondIncrementProperty);
BindUtils.RelayBind(this, ClockIdentifierProperty, presenter, TimePickerPresenter.ClockIdentifierProperty);
BindUtils.RelayBind(this, SelectedTimeProperty, presenter, TimePickerPresenter.SelectedTimeProperty);
BindUtils.RelayBind(this, IsNeedConfirmProperty, presenter, TimePickerPresenter.IsNeedConfirmProperty);
BindUtils.RelayBind(this, IsShowNowProperty, presenter, TimePickerPresenter.IsShowNowProperty);
}
protected override void NotifyFlyoutOpened()
{
base.NotifyFlyoutOpened();
if (_pickerPresenter is not null)
{
_pickerPresenter.ChoosingStatueChanged += HandleChoosingStatueChanged;
_pickerPresenter.HoverTimeChanged += HandleHoverTimeChanged;
_pickerPresenter.Confirmed += HandleConfirmed;
}
}
protected override void NotifyFlyoutAboutToClose(bool selectedIsValid)
{
base.NotifyFlyoutAboutToClose(selectedIsValid);
if (!selectedIsValid)
if (_pickerPresenter is not null)
{
if (SelectedTime.HasValue)
{
Text = DateTimeUtils.FormatTimeSpan(SelectedTime.Value,
ClockIdentifier == ClockIdentifierType.HourClock12);
}
else
{
Clear();
}
_pickerPresenter.ChoosingStatueChanged -= HandleChoosingStatueChanged;
_pickerPresenter.HoverTimeChanged -= HandleHoverTimeChanged;
_pickerPresenter.Confirmed -= HandleConfirmed;
}
}
private void HandleChoosingStatueChanged(object? sender, ChoosingStatusEventArgs args)
{
_isChoosing = args.IsChoosing;
UpdatePseudoClasses();
if (!args.IsChoosing)
{
ClearHoverSelectedInfo();
}
}
private void ClearHoverSelectedInfo()
{
Text = DateTimeUtils.FormatTimeSpan(SelectedTime,
ClockIdentifier == ClockIdentifierType.HourClock12);
}
private void HandleHoverTimeChanged(object? sender, TimeSelectedEventArgs args)
{
if (args.Time.HasValue)
{
Text = DateTimeUtils.FormatTimeSpan(args.Time.Value,
ClockIdentifier == ClockIdentifierType.HourClock12);
}
else
{
Text = null;
}
}
private void HandleConfirmed(object? sender, EventArgs args)
{
SelectedTime = _pickerPresenter?.SelectedTime;
ClosePickerFlyout();
}
/// <summary>
/// 清除时间选择器的值,不考虑默认值
@ -124,20 +200,10 @@ public class TimePicker : InfoPickerInput
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (VisualRoot is not null)
if (change.Property == SelectedTimeProperty)
{
if (change.Property == SelectedTimeProperty)
{
if (SelectedTime.HasValue)
{
Text = DateTimeUtils.FormatTimeSpan(SelectedTime.Value,
ClockIdentifier == ClockIdentifierType.HourClock12);
}
else
{
Clear();
}
}
Text = DateTimeUtils.FormatTimeSpan(SelectedTime,
ClockIdentifier == ClockIdentifierType.HourClock12);
}
}

View File

@ -5,28 +5,12 @@ namespace AtomUI.Controls;
internal class TimePickerFlyout : Flyout
{
internal TimePicker TimePickerRef { get; set; }
public TimePickerFlyout(TimePicker timePicker)
{
TimePickerRef = timePicker;
}
protected override Control CreatePresenter()
{
var presenter = new TimePickerFlyoutPresenter(TimePickerRef);
var presenter = new TimePickerFlyoutPresenter();
BindUtils.RelayBind(this, IsShowArrowEffectiveProperty, presenter, IsShowArrowProperty);
BindUtils.RelayBind(TimePickerRef, TimePicker.MinuteIncrementProperty, presenter,
TimePickerFlyoutPresenter.MinuteIncrementProperty);
BindUtils.RelayBind(TimePickerRef, TimePicker.SecondIncrementProperty, presenter,
TimePickerFlyoutPresenter.SecondIncrementProperty);
BindUtils.RelayBind(TimePickerRef, TimePicker.ClockIdentifierProperty, presenter,
TimePickerFlyoutPresenter.ClockIdentifierProperty);
BindUtils.RelayBind(TimePickerRef, TimePicker.SelectedTimeProperty, presenter,
TimePickerFlyoutPresenter.TimeProperty);
CalculateShowArrowEffective();
SetupArrowPosition(Popup, presenter);
return presenter;

View File

@ -1,116 +1,16 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace AtomUI.Controls;
internal class TimePickerFlyoutPresenter : FlyoutPresenter
{
public static readonly StyledProperty<int> MinuteIncrementProperty =
TimePicker.MinuteIncrementProperty.AddOwner<TimePickerFlyoutPresenter>();
public static readonly StyledProperty<int> SecondIncrementProperty =
TimePicker.SecondIncrementProperty.AddOwner<TimePickerFlyoutPresenter>();
public static readonly StyledProperty<TimeSpan> TimeProperty =
AvaloniaProperty.Register<TimePickerFlyoutPresenter, TimeSpan>(nameof(Time));
public static readonly StyledProperty<ClockIdentifierType> ClockIdentifierProperty =
TimePicker.ClockIdentifierProperty.AddOwner<TimePickerFlyoutPresenter>();
public int MinuteIncrement
{
get => GetValue(MinuteIncrementProperty);
set => SetValue(MinuteIncrementProperty, value);
}
public int SecondIncrement
{
get => GetValue(SecondIncrementProperty);
set => SetValue(SecondIncrementProperty, value);
}
public TimeSpan Time
{
get => GetValue(TimeProperty);
set => SetValue(TimeProperty, value);
}
public ClockIdentifierType ClockIdentifier
{
get => GetValue(ClockIdentifierProperty);
set => SetValue(ClockIdentifierProperty, value);
}
protected override Type StyleKeyOverride => typeof(TimePickerFlyoutPresenter);
internal TimePicker TimePickerRef { get; set; }
private TimePickerPresenter? _timePickerPresenter;
private IDisposable? _disposable;
private Button? _confirmButton;
private Button? _nowButton;
public TimePickerFlyoutPresenter(TimePicker timePicker)
{
TimePickerRef = timePicker;
HorizontalAlignment = HorizontalAlignment.Left;
}
internal TimePickerPresenter? TimePickerPresenter;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_timePickerPresenter = e.NameScope.Get<TimePickerPresenter>(ArrowDecoratedBoxTheme.ContentPresenterPart);
_confirmButton = e.NameScope.Get<Button>(TimePickerFlyoutPresenterTheme.ConfirmButtonPart);
_nowButton = e.NameScope.Get<Button>(TimePickerFlyoutPresenterTheme.NowButtonPart);
if (_timePickerPresenter is not null)
{
_timePickerPresenter.Confirmed += (sender, args) =>
{
TimePickerRef.NotifyConfirmed(_timePickerPresenter.Time);
};
}
if (_confirmButton is not null)
{
_confirmButton.Click += HandleConfirmButtonClicked;
}
if (_nowButton is not null)
{
_nowButton.Click += HandleNowButtonClicked;
}
}
private void HandleNowButtonClicked(object? sender, RoutedEventArgs args)
{
_timePickerPresenter?.NowConfirm();
TimePickerRef.ClosePickerFlyout();
}
private void HandleConfirmButtonClicked(object? sender, RoutedEventArgs args)
{
_timePickerPresenter?.Confirm();
TimePickerRef.ClosePickerFlyout();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (_timePickerPresenter is not null)
{
_disposable = TimePickerPresenter.TemporaryTimeProperty.Changed.Subscribe(args =>
{
TimePickerRef.NotifyTemporaryTimeSelected(args.GetNewValue<TimeSpan>());
});
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_disposable?.Dispose();
TimePickerPresenter = e.NameScope.Get<TimePickerPresenter>(TimePickerFlyoutPresenterTheme.TimePickerPresenterPart);
}
}

View File

@ -1,93 +1,30 @@
using AtomUI.Controls.Localization;
using AtomUI.Controls.TimePickerLang;
using AtomUI.Theme.Data;
using AtomUI.Theme.Styling;
using AtomUI.Theme.Styling;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Styling;
namespace AtomUI.Controls;
[ControlThemeProvider]
internal class TimePickerFlyoutPresenterTheme : ArrowDecoratedBoxTheme
{
public const string ContentLayoutPart = "PART_ContentLayout";
public const string NowButtonPart = "PART_NowButton";
public const string ConfirmButtonPart = "PART_ConfirmButton";
public const string ButtonsContainerPart = "PART_ButtonsContainer";
public const string TimePickerPresenterPart = "PART_TimePickerPresenter";
public TimePickerFlyoutPresenterTheme()
: base(typeof(TimePickerFlyoutPresenter))
: this(typeof(TimePickerFlyoutPresenter))
{
}
protected TimePickerFlyoutPresenterTheme(Type targetType) : base(targetType)
{
}
protected override Control BuildContent(INameScope scope)
{
var contentLayout = new DockPanel
var timePickerPresenter = new TimePickerPresenter()
{
Name = ContentLayoutPart
Name = TimePickerPresenterPart,
};
var contentPresenter = new TimePickerPresenter
{
Name = ContentPresenterPart,
IsShowHeader = false
};
CreateTemplateParentBinding(contentPresenter, TimePickerPresenter.MinuteIncrementProperty,
TimePickerFlyoutPresenter.MinuteIncrementProperty);
CreateTemplateParentBinding(contentPresenter, TimePickerPresenter.SecondIncrementProperty,
TimePickerFlyoutPresenter.SecondIncrementProperty);
CreateTemplateParentBinding(contentPresenter, TimePickerPresenter.ClockIdentifierProperty,
TimePickerFlyoutPresenter.ClockIdentifierProperty);
CreateTemplateParentBinding(contentPresenter, TimePickerPresenter.TimeProperty,
TimePickerFlyoutPresenter.TimeProperty);
contentPresenter.RegisterInNameScope(scope);
var buttons = new Panel
{
Name = ButtonsContainerPart
};
DockPanel.SetDock(buttons, Dock.Bottom);
var nowButton = new Button
{
ButtonType = ButtonType.Link,
Name = NowButtonPart,
HorizontalAlignment = HorizontalAlignment.Left,
SizeType = SizeType.Small
};
LanguageResourceBinder.CreateBinding(nowButton, Button.TextProperty, TimePickerLangResourceKey.Now);
nowButton.RegisterInNameScope(scope);
buttons.Children.Add(nowButton);
var confirmButton = new Button
{
Name = ConfirmButtonPart,
ButtonType = ButtonType.Primary,
SizeType = SizeType.Small,
HorizontalAlignment = HorizontalAlignment.Right
};
LanguageResourceBinder.CreateBinding(confirmButton, Button.TextProperty, CommonLangResourceKey.OkText);
confirmButton.RegisterInNameScope(scope);
buttons.Children.Add(confirmButton);
contentLayout.Children.Add(buttons);
contentLayout.Children.Add(contentPresenter);
return contentLayout;
}
protected override void BuildStyles()
{
base.BuildStyles();
var buttonsContainerStyle = new Style(selector => selector.Nesting().Template().Name(ButtonsContainerPart));
buttonsContainerStyle.Add(Layoutable.MarginProperty, TimePickerTokenResourceKey.ButtonsMargin);
Add(buttonsContainerStyle);
var contentPresenterStyle = new Style(selector => selector.Nesting().Template().Name(ContentPresenterPart));
contentPresenterStyle.Add(Layoutable.MaxWidthProperty, TimePickerTokenResourceKey.PickerPopupWidth);
contentPresenterStyle.Add(Layoutable.HeightProperty, TimePickerTokenResourceKey.PickerPopupHeight);
Add(contentPresenterStyle);
timePickerPresenter.RegisterInNameScope(scope);
return timePickerPresenter;
}
}

View File

@ -4,59 +4,48 @@ using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace AtomUI.Controls;
/// <summary>
/// Defines the presenter used for selecting a time. Intended for use with
/// <see cref="TimePicker" /> but can be used independently
/// </summary>
[TemplatePart(TimePickerPresenterTheme.HourSelectorPart, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TimePickerPresenterTheme.MinuteSelectorPart, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TimePickerPresenterTheme.SecondSelectorPart, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TimePickerPresenterTheme.PeriodHostPart, typeof(Panel), IsRequired = true)]
[TemplatePart(TimePickerPresenterTheme.PeriodSelectorPart, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TimePickerPresenterTheme.PickerContainerPart, typeof(Grid), IsRequired = true)]
[TemplatePart(TimePickerPresenterTheme.SecondSpacerPart, typeof(Rectangle), IsRequired = true)]
internal class TimePickerPresenter : PickerPresenterBase
{
#region
/// <summary>
/// Defines the <see cref="MinuteIncrement" /> property
/// </summary>
public static readonly StyledProperty<bool> IsNeedConfirmProperty =
TimePicker.IsNeedConfirmProperty.AddOwner<TimePickerPresenter>();
public static readonly StyledProperty<bool> IsShowNowProperty =
TimePicker.IsShowNowProperty.AddOwner<TimePickerPresenter>();
public static readonly StyledProperty<int> MinuteIncrementProperty =
TimePicker.MinuteIncrementProperty.AddOwner<TimePickerPresenter>();
public static readonly StyledProperty<int> SecondIncrementProperty =
TimePicker.SecondIncrementProperty.AddOwner<TimePickerPresenter>();
/// <summary>
/// Defines the <see cref="ClockIdentifier" /> property
/// </summary>
public static readonly StyledProperty<ClockIdentifierType> ClockIdentifierProperty =
TimePicker.ClockIdentifierProperty.AddOwner<TimePickerPresenter>();
/// <summary>
/// Defines the <see cref="Time" /> property
/// </summary>
public static readonly StyledProperty<TimeSpan> TimeProperty =
AvaloniaProperty.Register<TimePickerPresenter, TimeSpan>(nameof(Time));
public static readonly StyledProperty<TimeSpan> TemporaryTimeProperty =
AvaloniaProperty.Register<TimePickerPresenter, TimeSpan>(nameof(TemporaryTime));
public static readonly StyledProperty<bool> IsShowHeaderProperty =
AvaloniaProperty.Register<TimePickerPresenter, bool>(nameof(IsShowHeader), true);
/// <summary>
/// Gets or sets the minute increment in the selector
/// </summary>
public static readonly StyledProperty<TimeSpan?> SelectedTimeProperty =
TimePicker.SelectedTimeProperty.AddOwner<TimePickerPresenter>();
public bool IsNeedConfirm
{
get => GetValue(IsNeedConfirmProperty);
set => SetValue(IsNeedConfirmProperty, value);
}
public bool IsShowNow
{
get => GetValue(IsShowNowProperty);
set => SetValue(IsShowNowProperty, value);
}
public int MinuteIncrement
{
get => GetValue(MinuteIncrementProperty);
@ -68,163 +57,55 @@ internal class TimePickerPresenter : PickerPresenterBase
get => GetValue(SecondIncrementProperty);
set => SetValue(SecondIncrementProperty, value);
}
/// <summary>
/// Gets or sets the current clock identifier, either 12HourClock or 24HourClock
/// </summary>
public ClockIdentifierType ClockIdentifier
{
get => GetValue(ClockIdentifierProperty);
set => SetValue(ClockIdentifierProperty, value);
}
public TimeSpan? SelectedTime
{
get => GetValue(SelectedTimeProperty);
set => SetValue(SelectedTimeProperty, value);
}
#endregion
#region
internal static readonly DirectProperty<TimePickerPresenter, bool> ButtonsPanelVisibleProperty =
AvaloniaProperty.RegisterDirect<TimePickerPresenter, bool>(nameof(ButtonsPanelVisible),
o => o.ButtonsPanelVisible,
(o, v) => o.ButtonsPanelVisible = v);
private bool _buttonsPanelVisible = true;
internal bool ButtonsPanelVisible
{
get => _buttonsPanelVisible;
set => SetAndRaise(ButtonsPanelVisibleProperty, ref _buttonsPanelVisible, value);
}
#endregion
#region
/// <summary>
/// Gets or sets the current time
/// 当前 Pointer 选中的日期和时间的变化事件
/// </summary>
public TimeSpan Time
{
get => GetValue(TimeProperty);
set => SetValue(TimeProperty, value);
}
public TimeSpan TemporaryTime
{
get => GetValue(TemporaryTimeProperty);
set => SetValue(TemporaryTimeProperty, value);
}
public bool IsShowHeader
{
get => GetValue(IsShowHeaderProperty);
set => SetValue(IsShowHeaderProperty, value);
}
public event EventHandler<TimeSelectedEventArgs>? HoverTimeChanged;
/// <summary>
/// 当前是否处于选择中状态
/// </summary>
public event EventHandler<ChoosingStatusEventArgs>? ChoosingStatueChanged;
#endregion
#region
internal static readonly DirectProperty<TimePickerPresenter, double> SpacerThicknessProperty =
AvaloniaProperty.RegisterDirect<TimePickerPresenter, double>(nameof(SpacerWidth),
o => o.SpacerWidth,
(o, v) => o.SpacerWidth = v);
private double _spacerWidth;
public double SpacerWidth
{
get => _spacerWidth;
set => SetAndRaise(SpacerThicknessProperty, ref _spacerWidth, value);
}
#endregion
public TimePickerPresenter()
{
SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay);
}
static TimePickerPresenter()
{
KeyboardNavigation.TabNavigationProperty
.OverrideDefaultValue<TimePickerPresenter>(KeyboardNavigationMode.Cycle);
}
// TemplateItems
private Grid? _pickerContainer;
private Rectangle? _spacer3;
private Panel? _periodHost;
private TextBlock? _headerText;
private DateTimePickerPanel? _hourSelector;
private DateTimePickerPanel? _minuteSelector;
private DateTimePickerPanel? _secondSelector;
private DateTimePickerPanel? _periodSelector;
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
TokenResourceBinder.CreateGlobalTokenBinding(this, SpacerThicknessProperty, GlobalTokenResourceKey.LineWidth,
BindingPriority.Template,
new RenderScaleAwareDoubleConfigure(this));
InitPicker();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_pickerContainer = e.NameScope.Get<Grid>(TimePickerPresenterTheme.PickerContainerPart);
_periodHost = e.NameScope.Get<Panel>(TimePickerPresenterTheme.PeriodHostPart);
_headerText = e.NameScope.Get<TextBlock>(TimePickerPresenterTheme.HeaderTextPart);
_hourSelector = e.NameScope.Get<DateTimePickerPanel>(TimePickerPresenterTheme.HourSelectorPart);
_minuteSelector = e.NameScope.Get<DateTimePickerPanel>(TimePickerPresenterTheme.MinuteSelectorPart);
_secondSelector = e.NameScope.Get<DateTimePickerPanel>(TimePickerPresenterTheme.SecondSelectorPart);
_periodSelector = e.NameScope.Get<DateTimePickerPanel>(TimePickerPresenterTheme.PeriodSelectorPart);
if (_hourSelector is not null)
{
_hourSelector.SelectionChanged += HandleSelectionChanged;
}
if (_minuteSelector is not null)
{
_minuteSelector.SelectionChanged += HandleSelectionChanged;
}
if (_secondSelector is not null)
{
_secondSelector.SelectionChanged += HandleSelectionChanged;
}
if (_periodSelector is not null)
{
_periodSelector.SelectionChanged += HandleSelectionChanged;
}
_spacer3 = e.NameScope.Get<Rectangle>(TimePickerPresenterTheme.ThirdSpacerPart);
}
private void HandleSelectionChanged(object? sender, EventArgs args)
{
var selectedValue = CollectValue();
TemporaryTime = selectedValue;
if (IsShowHeader)
{
if (_headerText is not null)
{
_headerText.Text =
DateTimeUtils.FormatTimeSpan(selectedValue, ClockIdentifier == ClockIdentifierType.HourClock12);
}
}
}
private TimeSpan CollectValue()
{
var hour = _hourSelector!.SelectedValue;
var minute = _minuteSelector!.SelectedValue;
var second = _secondSelector!.SelectedValue;
var period = _periodSelector!.SelectedValue;
if (ClockIdentifier == ClockIdentifierType.HourClock12)
{
hour = period == 1 ? hour == 12 ? 12 : hour + 12 : period == 0 && hour == 12 ? 0 : hour;
}
return new TimeSpan(hour, minute, second);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == MinuteIncrementProperty ||
change.Property == SecondIncrementProperty ||
change.Property == ClockIdentifierProperty ||
change.Property == TimeProperty)
{
InitPicker();
}
}
private IDisposable? _choosingStateDisposable;
private Button? _nowButton;
private Button? _confirmButton;
private TimeView? _timeView;
protected override void OnKeyDown(KeyEventArgs e)
{
@ -251,64 +132,127 @@ internal class TimePickerPresenter : PickerPresenterBase
base.OnKeyDown(e);
}
protected override void OnConfirmed()
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
var value = CollectValue();
SetCurrentValue(TimeProperty, value);
base.OnConfirmed();
base.OnApplyTemplate(e);
_nowButton = e.NameScope.Get<Button>(TimePickerPresenterTheme.NowButtonPart);
_confirmButton = e.NameScope.Get<Button>(TimePickerPresenterTheme.ConfirmButtonPart);
_timeView = e.NameScope.Get<TimeView>(TimePickerPresenterTheme.TimeViewPart);
SetupButtonStatus();
if (_timeView is not null)
{
_timeView.HoverTimeChanged += HandleTimeViewDateHoverChanged;
_timeView.TimeSelected += HandleTimeViewDateSelected;
}
if (_nowButton is not null)
{
_nowButton.Click += HandleNowButtonClicked;
}
if (_confirmButton is not null)
{
_confirmButton.Click += HandleConfirmButtonClicked;
_confirmButton.IsEnabled = SelectedTime is not null;
_confirmButton.PointerEntered += (sender, args) =>
{
if (_timeView?.SelectedTime is not null)
{
HoverTimeChanged?.Invoke(this, new TimeSelectedEventArgs(_timeView?.SelectedTime));
}
};
_confirmButton.PointerExited += (sender, args) =>
{
ChoosingStatueChanged?.Invoke(this, new ChoosingStatusEventArgs(false));
};
}
}
internal void NowConfirm()
private void SetupButtonStatus()
{
SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay);
base.OnConfirmed();
}
internal void Confirm()
{
OnConfirmed();
}
internal void Dismiss()
{
OnDismiss();
}
private void InitPicker()
{
if (_pickerContainer == null)
if (_nowButton is null || _confirmButton is null)
{
return;
}
var clock12 = ClockIdentifier == ClockIdentifierType.HourClock12;
var use24HourClock = ClockIdentifier == ClockIdentifierType.HourClock24;
_hourSelector!.MaximumValue = clock12 ? 12 : 23;
_hourSelector.MinimumValue = clock12 ? 1 : 0;
_hourSelector.ItemFormat = "%h";
var hour = Time.Hours;
_hourSelector.SelectedValue = !clock12 ? hour :
hour > 12 ? hour - 12 :
hour == 0 ? 12 : hour;
_confirmButton.IsVisible = IsNeedConfirm;
if (IsShowNow)
{
if (!IsNeedConfirm)
{
_nowButton.HorizontalAlignment = HorizontalAlignment.Center;
}
else
{
_nowButton.HorizontalAlignment = HorizontalAlignment.Left;
}
}
else
{
_nowButton.IsVisible = false;
_nowButton.HorizontalAlignment = HorizontalAlignment.Left;
}
_minuteSelector!.MaximumValue = 59;
_minuteSelector.MinimumValue = 0;
_minuteSelector.Increment = MinuteIncrement;
_minuteSelector.SelectedValue = Time.Minutes;
_minuteSelector.ItemFormat = "mm";
ButtonsPanelVisible = _nowButton.IsVisible || _confirmButton.IsVisible;
}
_secondSelector!.MaximumValue = 59;
_secondSelector.MinimumValue = 0;
_secondSelector.Increment = SecondIncrement;
_secondSelector.SelectedValue = Time.Seconds;
_secondSelector.ItemFormat = "ss";
private void HandleNowButtonClicked(object? sender, RoutedEventArgs args)
{
SelectedTime = DateTime.Now.TimeOfDay;
if (!IsNeedConfirm)
{
OnConfirmed();
}
}
private void HandleConfirmButtonClicked(object? sender, RoutedEventArgs args)
{
if (SelectedTime is not null)
{
OnConfirmed();
}
}
private void HandleTimeViewDateHoverChanged(object? sender, TimeSelectedEventArgs args)
{
HoverTimeChanged?.Invoke(this, new TimeSelectedEventArgs(args.Time));
}
_periodSelector!.MaximumValue = 1;
_periodSelector.MinimumValue = 0;
_periodSelector.SelectedValue = hour >= 12 ? 1 : 0;
private void HandleTimeViewDateSelected(object? sender, TimeSelectedEventArgs args)
{
SelectedTime = args.Time;
if (!IsNeedConfirm)
{
OnConfirmed();
}
}
protected override void OnConfirmed()
{
base.OnConfirmed();
ChoosingStatueChanged?.Invoke(this, new ChoosingStatusEventArgs(false));
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
TokenResourceBinder.CreateGlobalTokenBinding(this, BorderThicknessProperty, GlobalTokenResourceKey.BorderThickness, BindingPriority.Template,
new RenderScaleAwareThicknessConfigure(this, thickness => new Thickness(0, thickness.Top, 0, 0)));
if (_timeView is not null)
{
_choosingStateDisposable = TimeView.IsPointerInSelectorProperty.Changed.Subscribe(args =>
{
ChoosingStatueChanged?.Invoke(this, new ChoosingStatusEventArgs(args.GetNewValue<bool>()));
});
}
}
_spacer3!.IsVisible = !use24HourClock;
_periodHost!.IsVisible = !use24HourClock;
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_choosingStateDisposable?.Dispose();
_choosingStateDisposable = null;
}
}

View File

@ -1,13 +1,11 @@
using AtomUI.Theme;
using AtomUI.Controls.DatePickerLang;
using AtomUI.Controls.Localization;
using AtomUI.Theme;
using AtomUI.Theme.Data;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
namespace AtomUI.Controls;
@ -15,23 +13,12 @@ namespace AtomUI.Controls;
[ControlThemeProvider]
internal class TimePickerPresenterTheme : BaseControlTheme
{
public const string MainContainerPart = "PART_MainContainer";
public const string PickerContainerPart = "PART_PickerContainer";
public const string HeaderTextPart = "PART_HeaderText";
public const string HourHostPart = "PART_HourHost";
public const string MinuteHostPart = "PART_MinuteHost";
public const string SecondHostPart = "PART_SecondHost";
public const string PeriodHostPart = "PART_PeriodHost";
public const string HourSelectorPart = "PART_HourSelector";
public const string MinuteSelectorPart = "PART_MinuteSelector";
public const string SecondSelectorPart = "PART_SecondSelector";
public const string PeriodSelectorPart = "PART_PeriodSelector";
public const string FirstSpacerPart = "PART_FirstSpacer";
public const string SecondSpacerPart = "PART_SecondSpacer";
public const string ThirdSpacerPart = "PART_ThirdSpacer";
public const string MainLayoutPart = "PART_MainLayout";
public const string NowButtonPart = "PART_NowButton";
public const string ConfirmButtonPart = "PART_ConfirmButton";
public const string ButtonsLayoutPart = "PART_ButtonsLayout";
public const string ButtonsFramePart = "PART_ButtonsFrame";
public const string TimeViewPart = "PART_TimeView";
public TimePickerPresenterTheme()
: base(typeof(TimePickerPresenter))
@ -42,222 +29,89 @@ internal class TimePickerPresenterTheme : BaseControlTheme
{
return new FuncControlTemplate<TimePickerPresenter>((presenter, scope) =>
{
var mainContainer = new Grid
var mainLayout = new DockPanel()
{
Name = MainContainerPart,
RowDefinitions = new RowDefinitions
{
new(GridLength.Auto),
new(GridLength.Auto),
new(GridLength.Star)
}
Name = MainLayoutPart,
LastChildFill = true
};
BuildHeader(mainContainer, scope);
BuildHosts(mainContainer, scope);
return mainContainer;
var calendarView = BuildTimeView(presenter, scope);
var buttonsContainerFrame = new Border()
{
Name = ButtonsFramePart
};
CreateTemplateParentBinding(buttonsContainerFrame, Border.BorderThicknessProperty, TimePickerPresenter.BorderThicknessProperty);
var buttonsPanel = BuildButtons(presenter, scope);
CreateTemplateParentBinding(buttonsContainerFrame, Border.IsVisibleProperty, TimePickerPresenter.ButtonsPanelVisibleProperty);
buttonsContainerFrame.Child = buttonsPanel;
DockPanel.SetDock(buttonsContainerFrame, Dock.Bottom);
mainLayout.Children.Add(buttonsContainerFrame);
mainLayout.Children.Add(calendarView);
return mainLayout;
});
}
private void BuildHeader(Grid mainContainer, INameScope scope)
protected virtual Control BuildTimeView(TimePickerPresenter presenter, INameScope scope)
{
var headerText = new TextBlock
var timeView = new TimeView()
{
Name = HeaderTextPart
Name = TimeViewPart,
IsShowHeader = false
};
headerText.RegisterInNameScope(scope);
CreateTemplateParentBinding(headerText, Visual.IsVisibleProperty, TimePickerPresenter.IsShowHeaderProperty);
mainContainer.Children.Add(headerText);
Grid.SetRow(headerText, 0);
var separator = new Rectangle
{
HorizontalAlignment = HorizontalAlignment.Stretch
};
CreateTemplateParentBinding(separator, Layoutable.HeightProperty, TimePickerPresenter.SpacerThicknessProperty);
CreateTemplateParentBinding(separator, Visual.IsVisibleProperty, TimePickerPresenter.IsShowHeaderProperty);
TokenResourceBinder.CreateGlobalTokenBinding(separator, Shape.FillProperty,
GlobalTokenResourceKey.ColorBorder);
mainContainer.Children.Add(separator);
Grid.SetRow(separator, 1);
CreateTemplateParentBinding(timeView, TimeView.SelectedTimeProperty, TimePickerPresenter.SelectedTimeProperty);
timeView.RegisterInNameScope(scope);
return timeView;
}
private void BuildHosts(Grid mainContainer, INameScope scope)
protected virtual Panel BuildButtons(TimePickerPresenter presenter, INameScope scope)
{
var pickerContainer = new Grid
var buttonsPanel = new Panel()
{
Name = PickerContainerPart,
ColumnDefinitions = new ColumnDefinitions
{
new(GridLength.Star),
new(GridLength.Auto),
new(GridLength.Star),
new(GridLength.Auto),
new(GridLength.Star),
new(GridLength.Auto),
new(GridLength.Star)
}
Name = ButtonsLayoutPart
};
pickerContainer.RegisterInNameScope(scope);
var hourHost = new Panel
var nowButton = new Button
{
Name = HourHostPart
ButtonType = ButtonType.Link,
Name = NowButtonPart,
HorizontalAlignment = HorizontalAlignment.Left,
SizeType = SizeType.Small
};
Grid.SetColumn(hourHost, 0);
hourHost.RegisterInNameScope(scope);
pickerContainer.Children.Add(hourHost);
{
var scrollViewer = new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden
};
var hourSelector = new DateTimePickerPanel
{
Name = HourSelectorPart,
PanelType = DateTimePickerPanelType.Hour,
ShouldLoop = true
};
hourSelector.RegisterInNameScope(scope);
TokenResourceBinder.CreateTokenBinding(hourSelector, DateTimePickerPanel.ItemHeightProperty,
TimePickerTokenResourceKey.ItemHeight);
scrollViewer.Content = hourSelector;
hourHost.Children.Add(scrollViewer);
}
LanguageResourceBinder.CreateBinding(nowButton, Button.TextProperty, DatePickerLangResourceKey.Now);
nowButton.RegisterInNameScope(scope);
buttonsPanel.Children.Add(nowButton);
var firstSpacer = new Rectangle
var confirmButton = new Button
{
Name = FirstSpacerPart,
HorizontalAlignment = HorizontalAlignment.Center
Name = ConfirmButtonPart,
ButtonType = ButtonType.Primary,
SizeType = SizeType.Small,
HorizontalAlignment = HorizontalAlignment.Right
};
firstSpacer.RegisterInNameScope(scope);
CreateTemplateParentBinding(firstSpacer, Layoutable.WidthProperty, TimePickerPresenter.SpacerThicknessProperty);
TokenResourceBinder.CreateGlobalTokenBinding(firstSpacer, Shape.FillProperty,
GlobalTokenResourceKey.ColorBorder);
Grid.SetColumn(firstSpacer, 1);
pickerContainer.Children.Add(firstSpacer);
var minuteHost = new Panel
{
Name = MinuteHostPart
};
Grid.SetColumn(minuteHost, 2);
minuteHost.RegisterInNameScope(scope);
pickerContainer.Children.Add(minuteHost);
{
var scrollViewer = new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden
};
var minuteSelector = new DateTimePickerPanel
{
Name = MinuteSelectorPart,
PanelType = DateTimePickerPanelType.Minute,
ShouldLoop = true
};
minuteSelector.RegisterInNameScope(scope);
TokenResourceBinder.CreateTokenBinding(minuteSelector, DateTimePickerPanel.ItemHeightProperty,
TimePickerTokenResourceKey.ItemHeight);
scrollViewer.Content = minuteSelector;
minuteHost.Children.Add(scrollViewer);
}
var secondSpacer = new Rectangle
{
Name = SecondSpacerPart,
HorizontalAlignment = HorizontalAlignment.Center
};
CreateTemplateParentBinding(secondSpacer, Layoutable.WidthProperty,
TimePickerPresenter.SpacerThicknessProperty);
secondSpacer.RegisterInNameScope(scope);
TokenResourceBinder.CreateGlobalTokenBinding(secondSpacer, Shape.FillProperty,
GlobalTokenResourceKey.ColorBorder);
Grid.SetColumn(secondSpacer, 3);
pickerContainer.Children.Add(secondSpacer);
var secondHost = new Panel
{
Name = SecondHostPart
};
Grid.SetColumn(secondHost, 4);
secondHost.RegisterInNameScope(scope);
pickerContainer.Children.Add(secondHost);
{
var scrollViewer = new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden
};
var secondSelector = new DateTimePickerPanel
{
Name = SecondSelectorPart,
PanelType = DateTimePickerPanelType.Second,
ShouldLoop = true
};
secondSelector.RegisterInNameScope(scope);
TokenResourceBinder.CreateTokenBinding(secondSelector, DateTimePickerPanel.ItemHeightProperty,
TimePickerTokenResourceKey.ItemHeight);
scrollViewer.Content = secondSelector;
secondHost.Children.Add(scrollViewer);
}
var thirdSpacer = new Rectangle
{
Name = ThirdSpacerPart,
HorizontalAlignment = HorizontalAlignment.Center
};
CreateTemplateParentBinding(thirdSpacer, Layoutable.WidthProperty, TimePickerPresenter.SpacerThicknessProperty);
thirdSpacer.RegisterInNameScope(scope);
Grid.SetColumn(thirdSpacer, 5);
pickerContainer.Children.Add(thirdSpacer);
TokenResourceBinder.CreateGlobalTokenBinding(thirdSpacer, Shape.FillProperty,
GlobalTokenResourceKey.ColorBorder);
var periodHost = new Panel
{
Name = PeriodHostPart
};
Grid.SetColumn(periodHost, 6);
periodHost.RegisterInNameScope(scope);
pickerContainer.Children.Add(periodHost);
{
var scrollViewer = new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden
};
var periodSelector = new DateTimePickerPanel
{
Name = PeriodSelectorPart,
PanelType = DateTimePickerPanelType.TimePeriod,
ShouldLoop = false
};
periodSelector.RegisterInNameScope(scope);
TokenResourceBinder.CreateTokenBinding(periodSelector, DateTimePickerPanel.ItemHeightProperty,
TimePickerTokenResourceKey.ItemHeight);
scrollViewer.Content = periodSelector;
periodHost.Children.Add(scrollViewer);
}
mainContainer.Children.Add(pickerContainer);
mainContainer.RegisterInNameScope(scope);
Grid.SetRow(pickerContainer, 2);
LanguageResourceBinder.CreateBinding(confirmButton, Button.TextProperty, CommonLangResourceKey.OkText);
confirmButton.RegisterInNameScope(scope);
buttonsPanel.Children.Add(confirmButton);
return buttonsPanel;
}
protected override void BuildStyles()
{
var commonStyle = new Style(selector => selector.Nesting());
var headerTextStyle = new Style(selector => selector.Nesting().Template().Name(HeaderTextPart));
headerTextStyle.Add(Layoutable.HeightProperty, TimePickerTokenResourceKey.ItemHeight);
headerTextStyle.Add(Layoutable.HorizontalAlignmentProperty, HorizontalAlignment.Center);
headerTextStyle.Add(Layoutable.VerticalAlignmentProperty, VerticalAlignment.Center);
headerTextStyle.Add(TextBlock.FontWeightProperty, FontWeight.SemiBold);
var commonStyle = new Style(selector => selector.Nesting());
commonStyle.Add(TimePickerPresenter.WidthProperty, TimePickerTokenResourceKey.PickerPopupWidth);
commonStyle.Add(TimePickerPresenter.HeightProperty, TimePickerTokenResourceKey.PickerPopupHeight);
var buttonsFrameStyle = new Style(selector => selector.Nesting().Template().Name(ButtonsFramePart));
buttonsFrameStyle.Add(Border.BorderBrushProperty, GlobalTokenResourceKey.ColorBorderSecondary);
commonStyle.Add(buttonsFrameStyle);
commonStyle.Add(headerTextStyle);
var buttonsPanelStyle = new Style(selector => selector.Nesting().Template().Name(ButtonsLayoutPart));
buttonsPanelStyle.Add(Panel.MarginProperty, DatePickerTokenResourceKey.ButtonsPanelMargin);
commonStyle.Add(buttonsPanelStyle);
Add(commonStyle);
}
}

View File

@ -1,6 +1,7 @@
using AtomUI.Controls.Internal;
using AtomUI.Theme.Styling;
using Avalonia.Controls;
using Avalonia.Styling;
namespace AtomUI.Controls;
@ -18,4 +19,13 @@ internal class TimePickerTheme : InfoPickerInputTheme
Kind = "ClockCircleOutlined"
};
}
protected override void BuildStyles()
{
base.BuildStyles();
var commonStyle = new Style(selector => selector.Nesting());
commonStyle.Add(TimePicker.MinWidthProperty, TimePickerTokenResourceKey.PickerInputMinWidth);
Add(commonStyle);
}
}

View File

@ -47,6 +47,11 @@ internal class TimePickerToken : AbstractControlDesignToken
/// 选择指示器厚度
/// </summary>
public double RangePickerIndicatorThickness { get; set; }
/// <summary>
/// 时间选择器最小的宽度
/// </summary>
public double PickerInputMinWidth { get; set; }
internal override void CalculateFromAlias()
{
@ -55,8 +60,9 @@ internal class TimePickerToken : AbstractControlDesignToken
ItemPadding = new Thickness(0, _globalToken.PaddingXXS);
ButtonsMargin = new Thickness(0, _globalToken.MarginXS, 0, 0);
PickerPopupWidth = 200;
PickerPopupHeight = ItemHeight * 7;
PickerPopupHeight = ItemHeight * 8;
RangePickerArrowMargin = new Thickness(_globalToken.MarginXS, 0);
RangePickerIndicatorThickness = _globalToken.LineWidthFocus;
PickerInputMinWidth = 120;
}
}

View File

@ -0,0 +1,379 @@
using AtomUI.Controls.Utils;
using AtomUI.Theme.Data;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Raw;
namespace AtomUI.Controls;
internal class TimeSelectedEventArgs : EventArgs
{
public TimeSpan? Time { get; }
public TimeSelectedEventArgs(TimeSpan? value)
{
Time = value;
}
}
[TemplatePart(TimeViewTheme.HourSelectorPart, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TimeViewTheme.MinuteSelectorPart, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TimeViewTheme.SecondSelectorPart, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TimeViewTheme.PeriodHostPart, typeof(Panel), IsRequired = true)]
[TemplatePart(TimeViewTheme.PeriodSelectorPart, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TimeViewTheme.PickerContainerPart, typeof(Grid), IsRequired = true)]
[TemplatePart(TimeViewTheme.SecondSpacerPart, typeof(Rectangle), IsRequired = true)]
internal class TimeView : TemplatedControl
{
#region
public static readonly StyledProperty<int> MinuteIncrementProperty =
TimePicker.MinuteIncrementProperty.AddOwner<TimeView>();
public static readonly StyledProperty<int> SecondIncrementProperty =
TimePicker.SecondIncrementProperty.AddOwner<TimeView>();
public static readonly StyledProperty<ClockIdentifierType> ClockIdentifierProperty =
TimePicker.ClockIdentifierProperty.AddOwner<TimeView>();
public static readonly StyledProperty<TimeSpan> SelectedTimeProperty =
AvaloniaProperty.Register<TimeView, TimeSpan>(nameof(SelectedTime));
public static readonly StyledProperty<bool> IsShowHeaderProperty =
AvaloniaProperty.Register<TimeView, bool>(nameof(IsShowHeader), true);
/// <summary>
/// Gets or sets the minute increment in the selector
/// </summary>
public int MinuteIncrement
{
get => GetValue(MinuteIncrementProperty);
set => SetValue(MinuteIncrementProperty, value);
}
public int SecondIncrement
{
get => GetValue(SecondIncrementProperty);
set => SetValue(SecondIncrementProperty, value);
}
/// <summary>
/// Gets or sets the current clock identifier, either 12HourClock or 24HourClock
/// </summary>
public ClockIdentifierType ClockIdentifier
{
get => GetValue(ClockIdentifierProperty);
set => SetValue(ClockIdentifierProperty, value);
}
public TimeSpan SelectedTime
{
get => GetValue(SelectedTimeProperty);
set => SetValue(SelectedTimeProperty, value);
}
public bool IsShowHeader
{
get => GetValue(IsShowHeaderProperty);
set => SetValue(IsShowHeaderProperty, value);
}
#endregion
#region
internal static readonly DirectProperty<TimeView, double> SpacerThicknessProperty =
AvaloniaProperty.RegisterDirect<TimeView, double>(nameof(SpacerWidth),
o => o.SpacerWidth,
(o, v) => o.SpacerWidth = v);
internal static readonly StyledProperty<bool> IsPointerInSelectorProperty =
AvaloniaProperty.Register<Calendar, bool>(nameof(IsPointerInSelector), false);
private double _spacerWidth;
public double SpacerWidth
{
get => _spacerWidth;
set => SetAndRaise(SpacerThicknessProperty, ref _spacerWidth, value);
}
internal bool IsPointerInSelector
{
get => GetValue(IsPointerInSelectorProperty);
set => SetValue(IsPointerInSelectorProperty, value);
}
#endregion
#region
public event EventHandler<TimeSelectedEventArgs>? TimeSelected;
public event EventHandler<TimeSelectedEventArgs>? HoverTimeChanged;
#endregion
static TimeView()
{
KeyboardNavigation.TabNavigationProperty
.OverrideDefaultValue<TimeView>(KeyboardNavigationMode.Cycle);
}
// TemplateItems
private Grid? _pickerContainer;
private Rectangle? _spacer3;
private Panel? _periodHost;
private TextBlock? _headerText;
private DateTimePickerPanel? _hourSelector;
private DateTimePickerPanel? _minuteSelector;
private DateTimePickerPanel? _secondSelector;
private DateTimePickerPanel? _periodSelector;
private IDisposable? _pointerPositionDisposable;
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
TokenResourceBinder.CreateGlobalTokenBinding(this, SpacerThicknessProperty, GlobalTokenResourceKey.LineWidth,
BindingPriority.Template,
new RenderScaleAwareDoubleConfigure(this));
var inputManager = AvaloniaLocator.Current.GetService<IInputManager>()!;
_pointerPositionDisposable = inputManager.Process.Subscribe(DetectPointerPosition);
}
private void DetectPointerPosition(RawInputEventArgs args)
{
if (args is RawPointerEventArgs pointerEventArgs)
{
if (!CheckPointerInSelectors(pointerEventArgs.Position))
{
IsPointerInSelector = false;
}
else
{
IsPointerInSelector = true;
}
}
}
protected virtual bool CheckPointerInSelectors(Point position)
{
if (ClockIdentifier == ClockIdentifierType.HourClock12)
{
return CheckPointerInSelector(_hourSelector, position) ||
CheckPointerInSelector(_minuteSelector, position) ||
CheckPointerInSelector(_secondSelector, position) ||
CheckPointerInSelector(_periodSelector, position);
}
return CheckPointerInSelector(_hourSelector, position) ||
CheckPointerInSelector(_minuteSelector, position) ||
CheckPointerInSelector(_secondSelector, position);
}
private bool CheckPointerInSelector(DateTimePickerPanel? selector, Point position)
{
if (selector is null)
{
return false;
}
var globalRect = GetSelectorGlobalRect(selector);
return globalRect.Contains(position);
}
private Rect GetSelectorGlobalRect(DateTimePickerPanel selector)
{
var pos = selector.TranslatePoint(new Point(0, 0), TopLevel.GetTopLevel(selector)!) ?? default;
return new Rect(pos, selector.Bounds.Size);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_pointerPositionDisposable?.Dispose();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_pickerContainer = e.NameScope.Get<Grid>(TimeViewTheme.PickerContainerPart);
_periodHost = e.NameScope.Get<Panel>(TimeViewTheme.PeriodHostPart);
_headerText = e.NameScope.Get<TextBlock>(TimeViewTheme.HeaderTextPart);
_hourSelector = e.NameScope.Get<DateTimePickerPanel>(TimeViewTheme.HourSelectorPart);
_minuteSelector = e.NameScope.Get<DateTimePickerPanel>(TimeViewTheme.MinuteSelectorPart);
_secondSelector = e.NameScope.Get<DateTimePickerPanel>(TimeViewTheme.SecondSelectorPart);
_periodSelector = e.NameScope.Get<DateTimePickerPanel>(TimeViewTheme.PeriodSelectorPart);
if (_hourSelector is not null)
{
_hourSelector.SelectionChanged += HandleSelectionChanged;
_hourSelector.CellHovered += HandleSelectorCellHovered;
}
if (_minuteSelector is not null)
{
_minuteSelector.SelectionChanged += HandleSelectionChanged;
_minuteSelector.CellHovered += HandleSelectorCellHovered;
}
if (_secondSelector is not null)
{
_secondSelector.SelectionChanged += HandleSelectionChanged;
_secondSelector.CellHovered += HandleSelectorCellHovered;
}
if (_periodSelector is not null)
{
_periodSelector.SelectionChanged += HandleSelectionChanged;
_periodSelector.CellHovered += HandleSelectorCellHovered;
}
_spacer3 = e.NameScope.Get<Rectangle>(TimeViewTheme.ThirdSpacerPart);
InitPicker();
}
private void HandleSelectorCellHovered(object? sender, CellHoverEventArgs args)
{
var selectedTime = CollectValue(false);
var hour = selectedTime.Hours;
var minute = selectedTime.Minutes;
var second = selectedTime.Seconds;
var period = _secondSelector?.SelectedValue ?? default;
var cellHoverInfo = args.CellHoverInfo;
if (cellHoverInfo.HasValue)
{
var panelType = cellHoverInfo.Value.PanelType;
var cellValue = cellHoverInfo.Value.CellValue;
if (panelType == DateTimePickerPanelType.Hour)
{
hour = cellValue;
if (ClockIdentifier == ClockIdentifierType.HourClock12)
{
if (period == 0 && hour == 12)
{
// AM
hour = 0;
}
if (period == 1 && hour != 12)
{
hour += 12;
}
}
} else if (panelType == DateTimePickerPanelType.Minute)
{
minute = cellValue;
} else if (panelType == DateTimePickerPanelType.Second)
{
second = cellValue;
} else if (panelType == DateTimePickerPanelType.TimePeriod)
{
period = cellValue;
if (period == 0 && hour == 12)
{
// AM
hour = 0;
}
if (period == 1 && hour != 12)
{
hour += 12;
}
}
var hoverTime = new TimeSpan(hour, minute, second);
HoverTimeChanged?.Invoke(this, new TimeSelectedEventArgs(hoverTime));
}
}
private void HandleSelectionChanged(object? sender, EventArgs args)
{
var selectedValue = CollectValue();
if (IsShowHeader)
{
if (_headerText is not null)
{
_headerText.Text =
DateTimeUtils.FormatTimeSpan(selectedValue, ClockIdentifier == ClockIdentifierType.HourClock12);
}
}
}
private TimeSpan CollectValue(bool translate = true)
{
var hour = _hourSelector!.SelectedValue;
var minute = _minuteSelector!.SelectedValue;
var second = _secondSelector!.SelectedValue;
var period = _periodSelector!.SelectedValue;
if (translate)
{
if (ClockIdentifier == ClockIdentifierType.HourClock12)
{
hour = period == 1 ? hour == 12 ? 12 : hour + 12 : period == 0 && hour == 12 ? 0 : hour;
}
}
return new TimeSpan(hour, minute, second);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == MinuteIncrementProperty ||
change.Property == SecondIncrementProperty ||
change.Property == ClockIdentifierProperty ||
change.Property == SelectedTimeProperty)
{
InitPicker();
}
}
private void InitPicker()
{
if (_pickerContainer == null)
{
return;
}
var clock12 = ClockIdentifier == ClockIdentifierType.HourClock12;
var use24HourClock = ClockIdentifier == ClockIdentifierType.HourClock24;
_hourSelector!.MaximumValue = clock12 ? 12 : 23;
_hourSelector.MinimumValue = clock12 ? 1 : 0;
_hourSelector.ItemFormat = "%h";
var hour = SelectedTime.Hours;
_hourSelector.SelectedValue = !clock12 ? hour :
hour > 12 ? hour - 12 :
hour == 0 ? 12 : hour;
_minuteSelector!.MaximumValue = 59;
_minuteSelector.MinimumValue = 0;
_minuteSelector.Increment = MinuteIncrement;
_minuteSelector.SelectedValue = SelectedTime.Minutes;
_minuteSelector.ItemFormat = "mm";
_secondSelector!.MaximumValue = 59;
_secondSelector.MinimumValue = 0;
_secondSelector.Increment = SecondIncrement;
_secondSelector.SelectedValue = SelectedTime.Seconds;
_secondSelector.ItemFormat = "ss";
_periodSelector!.MaximumValue = 1;
_periodSelector.MinimumValue = 0;
_periodSelector.SelectedValue = hour >= 12 ? 1 : 0;
_spacer3!.IsVisible = !use24HourClock;
_periodHost!.IsVisible = !use24HourClock;
}
}

View File

@ -0,0 +1,263 @@
using AtomUI.Theme;
using AtomUI.Theme.Styling;
using AtomUI.Utils;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
namespace AtomUI.Controls;
[ControlThemeProvider]
internal class TimeViewTheme : BaseControlTheme
{
public const string MainContainerPart = "PART_MainContainer";
public const string PickerContainerPart = "PART_PickerContainer";
public const string HeaderTextPart = "PART_HeaderText";
public const string HourHostPart = "PART_HourHost";
public const string MinuteHostPart = "PART_MinuteHost";
public const string SecondHostPart = "PART_SecondHost";
public const string PeriodHostPart = "PART_PeriodHost";
public const string HourSelectorPart = "PART_HourSelector";
public const string MinuteSelectorPart = "PART_MinuteSelector";
public const string SecondSelectorPart = "PART_SecondSelector";
public const string PeriodSelectorPart = "PART_PeriodSelector";
public const string FirstSpacerPart = "PART_FirstSpacer";
public const string SecondSpacerPart = "PART_SecondSpacer";
public const string ThirdSpacerPart = "PART_ThirdSpacer";
public TimeViewTheme()
: base(typeof(TimeView))
{
}
protected override IControlTemplate BuildControlTemplate()
{
return new FuncControlTemplate<TimeView>((presenter, scope) =>
{
var mainContainer = new Grid
{
Name = MainContainerPart,
RowDefinitions = new RowDefinitions
{
new(GridLength.Auto),
new(GridLength.Auto),
new(GridLength.Star)
}
};
BuildHeader(mainContainer, scope);
BuildHosts(mainContainer, scope);
return mainContainer;
});
}
private void BuildHeader(Grid mainContainer, INameScope scope)
{
var headerText = new TextBlock
{
Name = HeaderTextPart
};
headerText.RegisterInNameScope(scope);
CreateTemplateParentBinding(headerText, Visual.IsVisibleProperty, TimeView.IsShowHeaderProperty);
mainContainer.Children.Add(headerText);
Grid.SetRow(headerText, 0);
var separator = new Rectangle
{
HorizontalAlignment = HorizontalAlignment.Stretch
};
CreateTemplateParentBinding(separator, Layoutable.HeightProperty, TimeView.SpacerThicknessProperty);
CreateTemplateParentBinding(separator, Visual.IsVisibleProperty, TimeView.IsShowHeaderProperty);
TokenResourceBinder.CreateGlobalTokenBinding(separator, Shape.FillProperty,
GlobalTokenResourceKey.ColorBorder);
mainContainer.Children.Add(separator);
Grid.SetRow(separator, 1);
}
private void BuildHosts(Grid mainContainer, INameScope scope)
{
var pickerContainer = new Grid
{
Name = PickerContainerPart,
ColumnDefinitions = new ColumnDefinitions
{
new(GridLength.Star),
new(GridLength.Auto),
new(GridLength.Star),
new(GridLength.Auto),
new(GridLength.Star),
new(GridLength.Auto),
new(GridLength.Star)
}
};
pickerContainer.RegisterInNameScope(scope);
var hourHost = new Panel
{
Name = HourHostPart
};
Grid.SetColumn(hourHost, 0);
hourHost.RegisterInNameScope(scope);
pickerContainer.Children.Add(hourHost);
{
var scrollViewer = new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden
};
var hourSelector = new DateTimePickerPanel
{
Name = HourSelectorPart,
PanelType = DateTimePickerPanelType.Hour,
ShouldLoop = true
};
hourSelector.RegisterInNameScope(scope);
TokenResourceBinder.CreateTokenBinding(hourSelector, DateTimePickerPanel.ItemHeightProperty,
TimePickerTokenResourceKey.ItemHeight);
scrollViewer.Content = hourSelector;
hourHost.Children.Add(scrollViewer);
}
var firstSpacer = new Rectangle
{
Name = FirstSpacerPart,
HorizontalAlignment = HorizontalAlignment.Center
};
firstSpacer.RegisterInNameScope(scope);
CreateTemplateParentBinding(firstSpacer, Layoutable.WidthProperty, TimeView.SpacerThicknessProperty);
TokenResourceBinder.CreateGlobalTokenBinding(firstSpacer, Shape.FillProperty,
GlobalTokenResourceKey.ColorBorder);
Grid.SetColumn(firstSpacer, 1);
pickerContainer.Children.Add(firstSpacer);
var minuteHost = new Panel
{
Name = MinuteHostPart
};
Grid.SetColumn(minuteHost, 2);
minuteHost.RegisterInNameScope(scope);
pickerContainer.Children.Add(minuteHost);
{
var scrollViewer = new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden
};
var minuteSelector = new DateTimePickerPanel
{
Name = MinuteSelectorPart,
PanelType = DateTimePickerPanelType.Minute,
ShouldLoop = true
};
minuteSelector.RegisterInNameScope(scope);
TokenResourceBinder.CreateTokenBinding(minuteSelector, DateTimePickerPanel.ItemHeightProperty,
TimePickerTokenResourceKey.ItemHeight);
scrollViewer.Content = minuteSelector;
minuteHost.Children.Add(scrollViewer);
}
var secondSpacer = new Rectangle
{
Name = SecondSpacerPart,
HorizontalAlignment = HorizontalAlignment.Center
};
CreateTemplateParentBinding(secondSpacer, Layoutable.WidthProperty,
TimeView.SpacerThicknessProperty);
secondSpacer.RegisterInNameScope(scope);
TokenResourceBinder.CreateGlobalTokenBinding(secondSpacer, Shape.FillProperty,
GlobalTokenResourceKey.ColorBorder);
Grid.SetColumn(secondSpacer, 3);
pickerContainer.Children.Add(secondSpacer);
var secondHost = new Panel
{
Name = SecondHostPart
};
Grid.SetColumn(secondHost, 4);
secondHost.RegisterInNameScope(scope);
pickerContainer.Children.Add(secondHost);
{
var scrollViewer = new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden
};
var secondSelector = new DateTimePickerPanel
{
Name = SecondSelectorPart,
PanelType = DateTimePickerPanelType.Second,
ShouldLoop = true
};
secondSelector.RegisterInNameScope(scope);
TokenResourceBinder.CreateTokenBinding(secondSelector, DateTimePickerPanel.ItemHeightProperty,
TimePickerTokenResourceKey.ItemHeight);
scrollViewer.Content = secondSelector;
secondHost.Children.Add(scrollViewer);
}
var thirdSpacer = new Rectangle
{
Name = ThirdSpacerPart,
HorizontalAlignment = HorizontalAlignment.Center
};
CreateTemplateParentBinding(thirdSpacer, Layoutable.WidthProperty, TimeView.SpacerThicknessProperty);
thirdSpacer.RegisterInNameScope(scope);
Grid.SetColumn(thirdSpacer, 5);
pickerContainer.Children.Add(thirdSpacer);
TokenResourceBinder.CreateGlobalTokenBinding(thirdSpacer, Shape.FillProperty,
GlobalTokenResourceKey.ColorBorder);
var periodHost = new Panel
{
Name = PeriodHostPart
};
Grid.SetColumn(periodHost, 6);
periodHost.RegisterInNameScope(scope);
pickerContainer.Children.Add(periodHost);
{
var scrollViewer = new ScrollViewer
{
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
VerticalScrollBarVisibility = ScrollBarVisibility.Hidden
};
var periodSelector = new DateTimePickerPanel
{
Name = PeriodSelectorPart,
PanelType = DateTimePickerPanelType.TimePeriod,
ShouldLoop = false
};
periodSelector.RegisterInNameScope(scope);
TokenResourceBinder.CreateTokenBinding(periodSelector, DateTimePickerPanel.ItemHeightProperty,
TimePickerTokenResourceKey.ItemHeight);
scrollViewer.Content = periodSelector;
periodHost.Children.Add(scrollViewer);
}
mainContainer.Children.Add(pickerContainer);
mainContainer.RegisterInNameScope(scope);
Grid.SetRow(pickerContainer, 2);
}
protected override void BuildStyles()
{
var commonStyle = new Style(selector => selector.Nesting());
var headerTextStyle = new Style(selector => selector.Nesting().Template().Name(HeaderTextPart));
headerTextStyle.Add(Layoutable.HeightProperty, TimePickerTokenResourceKey.ItemHeight);
headerTextStyle.Add(Layoutable.HorizontalAlignmentProperty, HorizontalAlignment.Center);
headerTextStyle.Add(Layoutable.VerticalAlignmentProperty, VerticalAlignment.Center);
headerTextStyle.Add(TextBlock.FontWeightProperty, FontWeight.SemiBold);
commonStyle.Add(headerTextStyle);
Add(commonStyle);
}
}

View File

@ -6,9 +6,13 @@ namespace AtomUI.Controls.Utils;
internal static class DateTimeUtils
{
public static string FormatTimeSpan(TimeSpan value, bool is12HourClock = false)
public static string FormatTimeSpan(TimeSpan? time, bool is12HourClock = false)
{
var dateTime = DateTime.Today.Add(value);
if (time is null)
{
return string.Empty;
}
var dateTime = DateTime.Today.Add(time.Value);
if (is12HourClock)
{
var formatInfo = new DateTimeFormatInfo();