Merge pull request #227 from irihitech/timepicker

New TimePicker
This commit is contained in:
Dong Bin
2024-04-28 18:16:47 +08:00
committed by GitHub
13 changed files with 942 additions and 1 deletions

View File

@@ -0,0 +1,30 @@
<UserControl
x:Class="Ursa.Demo.Pages.TimePickerDemo"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:u="https://irihi.tech/ursa"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d">
<StackPanel HorizontalAlignment="Left">
<ToggleSwitch Name="needConfirm" Content="Need Confirm" />
<TextBlock Text="{Binding #picker.SelectedTime}" />
<u:TimePicker
Name="picker"
Width="200"
HorizontalAlignment="Left"
NeedConfirmation="{Binding #needConfirm.IsChecked}"
PanelFormat="hh mm tt" />
<u:TimePicker
Width="300"
Classes="ClearButton"
DisplayFormat="HH 时 mm 分 ss 秒"
PanelFormat="tt HH mm ss"
HorizontalAlignment="Left"
NeedConfirmation="True"
InnerLeftContent="时刻"
InnerRightContent="截止" />
</StackPanel>
</UserControl>

View File

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

View File

@@ -55,6 +55,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeySelectionList => new SelectionListDemoViewModel(),
MenuKeys.MenuKeySkeleton => new SkeletonDemoViewModel(),
MenuKeys.MenuKeyTagInput => new TagInputDemoViewModel(),
MenuKeys.MenuKeyTimePicker => new TimePickerDemoViewModel(),
MenuKeys.MenuKeyTimeline => new TimelineDemoViewModel(),
MenuKeys.MenuKeyTreeComboBox => new TreeComboBoxDemoViewModel(),
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),

View File

@@ -43,6 +43,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Skeleton", Key = MenuKeys.MenuKeySkeleton },
new() { MenuHeader = "TagInput", Key = MenuKeys.MenuKeyTagInput },
new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler },
new() { MenuHeader = "TimePicker", Key = MenuKeys.MenuKeyTimePicker },
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline },
new() { MenuHeader = "TreeComboBox", Key = MenuKeys.MenuKeyTreeComboBox },
new() { MenuHeader = "TwoTonePathIcon", Key = MenuKeys.MenuKeyTwoTonePathIcon},
@@ -86,6 +87,7 @@ public static class MenuKeys
public const string MenuKeySelectionList = "SelectionList";
public const string MenuKeyTagInput = "TagInput";
public const string MenuKeySkeleton = "Skeleton";
public const string MenuKeyTimePicker = "TimePicker";
public const string MenuKeyTimeline = "Timeline";
public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon";
public const string MenuKeyThemeToggler = "ThemeToggler";

View File

@@ -0,0 +1,8 @@
using CommunityToolkit.Mvvm.ComponentModel;
namespace Ursa.Demo.ViewModels;
public class TimePickerDemoViewModel: ObservableObject
{
}

View File

@@ -0,0 +1,270 @@
<ResourceDictionary
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="clr-namespace:Ursa.Converters;assembly=Ursa"
xmlns:iri="https://irihi.tech/shared"
xmlns:u="https://irihi.tech/ursa">
<!-- Add Resources Here -->
<Design.PreviewWith>
<u:TimePickerPresenter Height="300" />
</Design.PreviewWith>
<ControlTheme x:Key="{x:Type u:TimePickerPresenter}" TargetType="u:TimePickerPresenter">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="MaxHeight" Value="300" />
<Setter Property="Template">
<ControlTemplate TargetType="u:TimePickerPresenter">
<Grid Name="{x:Static u:TimePickerPresenter.PART_PickerContainer}" ColumnDefinitions="*, Auto, *, Auto, *, Auto, *">
<Grid.Styles>
<Style Selector="u|UrsaDateTimeScrollPanel &gt; ListBoxItem">
<Setter Property="Theme" Value="{DynamicResource DateTimePickerItem}" />
</Style>
</Grid.Styles>
<ScrollViewer
Name="{x:Static u:TimePickerPresenter.PART_HourScrollPanel}"
Grid.Column="0"
HorizontalAlignment="Left"
HorizontalContentAlignment="Left"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<u:UrsaDateTimeScrollPanel
Name="{x:Static u:TimePickerPresenter.PART_HourSelector}"
MinWidth="64"
HorizontalAlignment="Left"
ItemHeight="32"
PanelType="Hour"
ShouldLoop="True" />
</ScrollViewer>
<Rectangle
Name="{x:Static u:TimePickerPresenter.PART_FirstSeparator}"
Grid.Column="1"
Width="1"
Margin="0,4"
VerticalAlignment="Stretch"
Fill="{DynamicResource DateTimePickerSeparatorBackground}" />
<ScrollViewer
Name="{x:Static u:TimePickerPresenter.PART_MinuteScrollPanel}"
Grid.Column="2"
HorizontalAlignment="Left"
HorizontalContentAlignment="Left"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<u:UrsaDateTimeScrollPanel
Name="{x:Static u:TimePickerPresenter.PART_MinuteSelector}"
MinWidth="64"
ItemHeight="32"
PanelType="Minute"
ShouldLoop="True" />
</ScrollViewer>
<Rectangle
Name="{x:Static u:TimePickerPresenter.PART_SecondSeparator}"
Grid.Column="3"
Width="1"
Margin="0,4"
VerticalAlignment="Stretch"
Fill="{DynamicResource DateTimePickerSeparatorBackground}" />
<ScrollViewer
Name="{x:Static u:TimePickerPresenter.PART_SecondScrollPanel}"
Grid.Column="4"
HorizontalAlignment="Left"
HorizontalContentAlignment="Left"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<u:UrsaDateTimeScrollPanel
Name="{x:Static u:TimePickerPresenter.PART_SecondSelector}"
MinWidth="64"
ItemHeight="32"
PanelType="Minute"
ShouldLoop="True" />
</ScrollViewer>
<Rectangle
Name="{x:Static u:TimePickerPresenter.PART_ThirdSeparator}"
Grid.Column="5"
Width="1"
Margin="0,4"
VerticalAlignment="Stretch"
Fill="{DynamicResource DateTimePickerSeparatorBackground}" />
<ScrollViewer
Name="{x:Static u:TimePickerPresenter.PART_AmPmScrollPanel}"
Grid.Column="6"
HorizontalAlignment="Left"
HorizontalContentAlignment="Left"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Hidden">
<u:UrsaDateTimeScrollPanel
Name="{x:Static u:TimePickerPresenter.PART_AmPmSelector}"
MinWidth="64"
ItemHeight="32"
PanelType="TimePeriod" />
</ScrollViewer>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:TimePicker}" TargetType="u:TimePicker">
<Setter Property="Background" Value="{DynamicResource TextBoxDefaultBackground}" />
<Setter Property="Foreground" Value="{DynamicResource TextBoxForeground}" />
<Setter Property="BorderBrush" Value="{DynamicResource TextBoxDefaultBorderBrush}" />
<Setter Property="BorderThickness" Value="{DynamicResource TextBoxBorderThickness}" />
<Setter Property="CornerRadius" Value="{DynamicResource TextBoxDefaultCornerRadius}" />
<Setter Property="MinHeight" Value="32"/>
<Setter Property="Template">
<ControlTemplate TargetType="u:TimePicker">
<DataValidationErrors>
<Panel
x:Name="LayoutRoot"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<Border
x:Name="Background"
HorizontalAlignment="Stretch"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}" />
<Grid ColumnDefinitions="*, Auto">
<TextBox
Name="{x:Static u:TimePicker.PART_TextBox}"
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Background="Transparent"
BorderBrush="Transparent"
BorderThickness="0"
Foreground="{TemplateBinding Foreground}"
InnerLeftContent="{TemplateBinding InnerLeftContent}"
InnerRightContent="{TemplateBinding InnerRightContent}"
IsReadOnly="{TemplateBinding IsReadonly}"
Theme="{DynamicResource NoErrorTextBox}"
Watermark="{TemplateBinding Watermark}">
<TextBox.Styles>
<Style Selector="TextBox#PART_TextBox:pointerover /template/ Border#PART_ContentPresenterBorder">
<!-- By default the TextBox has its own focused state, override this to disable it here -->
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="TextBox#PART_TextBox:focus /template/ Border#PART_ContentPresenterBorder">
<!-- By default the TextBox has its own focused state, override this to disable it here -->
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="TextBox#PART_TextBox:disabled">
<Style Selector="^ /template/ Border#PART_ContentPresenterBorder">
<!-- By default the TextBox has its own disabled state, override this to make the border background show through -->
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
</Style>
</TextBox.Styles>
</TextBox>
<Button
Name="ClearButton"
Grid.Column="1"
Padding="0,0,8,0"
Command="{Binding $parent[iri:IClearControl].Clear}"
Content="{DynamicResource IconButtonClearData}"
Focusable="False"
IsVisible="False"
Theme="{DynamicResource InnerIconButton}" />
<Button
Name="{x:Static u:TimePicker.PART_Button}"
Grid.Column="1"
Padding="0,0,8,0"
Content="{DynamicResource TimePickerIconGlyph}"
Focusable="False"
Theme="{DynamicResource InnerIconButton}" />
<Popup
Name="{x:Static u:TimePicker.PART_Popup}"
Grid.Column="0"
IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsDropdownOpen,
Mode=TwoWay}"
Placement="BottomEdgeAlignedLeft"
PlacementTarget="Background">
<Border
Margin="0,4"
HorizontalAlignment="Stretch"
Background="{DynamicResource ComboBoxPopupBackground}"
BorderBrush="{DynamicResource ComboBoxPopupBorderBrush}"
BorderThickness="{DynamicResource ComboBoxPopupBorderThickness}"
BoxShadow="{DynamicResource ComboBoxPopupBoxShadow}"
ClipToBounds="True"
CornerRadius="6">
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" IsVisible="{TemplateBinding NeedConfirmation}">
<Button
Margin="8"
HorizontalAlignment="Right"
Command="{Binding $parent[u:TimePicker].Confirm}"
Content="{DynamicResource STRING_DATE_TIME_CONFIRM}" />
</StackPanel>
<ContentPresenter
Name="PART_PopupHeader"
Margin="8,8,8,0"
Content="{TemplateBinding PopupInnerTopContent}"
DockPanel.Dock="Top"
IsVisible="{TemplateBinding PopupInnerTopContent,
Converter={x:Static ObjectConverters.IsNotNull}}" />
<ContentPresenter
Name="PART_PopupFooter"
Margin="8,0,8,8"
Content="{TemplateBinding PopupInnerBottomContent}"
DockPanel.Dock="Bottom"
IsVisible="{TemplateBinding PopupInnerBottomContent,
Converter={x:Static ObjectConverters.IsNotNull}}" />
<u:TimePickerPresenter
Name="{x:Static u:TimePicker.PART_Presenter}"
NeedsConfirmation="{TemplateBinding NeedConfirmation}"
PanelFormat="{TemplateBinding PanelFormat}"
Time="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=SelectedTime, Mode=OneWayToSource}" />
</DockPanel>
</Border>
</Popup>
</Grid>
</Panel>
</DataValidationErrors>
</ControlTemplate>
</Setter>
<Style Selector="^.clearButton, ^.ClearButton">
<Style Selector="^:pointerover /template/ Button#ClearButton">
<Setter Property="IsVisible" Value="{Binding $parent[u:TimePicker].SelectedTime, Converter={x:Static ObjectConverters.IsNotNull}}" />
</Style>
<Style Selector="^:pointerover /template/ Button#PART_Button">
<Setter Property="IsVisible" Value="{Binding $parent[u:TimePicker].SelectedTime, Converter={x:Static ObjectConverters.IsNull}}"/>
</Style>
</Style>
<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

@@ -32,6 +32,7 @@
<ResourceInclude Source="SelectionList.axaml" />
<ResourceInclude Source="TagInput.axaml" />
<ResourceInclude Source="ThemeSelector.axaml" />
<ResourceInclude Source="TimePicker.axaml" />
<ResourceInclude Source="Timeline.axaml" />
<ResourceInclude Source="TreeComboBox.axaml"/>
<ResourceInclude Source="Skeleton.axaml" />

View File

@@ -15,4 +15,5 @@
<x:String x:Key="STRING_THEME_TOGGLE_DARK">Dark</x:String>
<x:String x:Key="STRING_THEME_TOGGLE_LIGHT">Light</x:String>
<x:String x:Key="STRING_THEME_TOGGLE_SYSTEM">System</x:String>
<x:String x:Key="STRING_DATE_TIME_CONFIRM">Confirm</x:String>
</ResourceDictionary>

View File

@@ -15,4 +15,5 @@
<x:String x:Key="STRING_THEME_TOGGLE_DARK">暗色</x:String>
<x:String x:Key="STRING_THEME_TOGGLE_LIGHT">亮色</x:String>
<x:String x:Key="STRING_THEME_TOGGLE_SYSTEM">跟随系统</x:String>
<x:String x:Key="STRING_DATE_TIME_CONFIRM">确认</x:String>
</ResourceDictionary>

View File

@@ -0,0 +1,264 @@
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Contracts;
using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls;
[TemplatePart(PART_TextBox, typeof(TextBox))]
[TemplatePart(PART_Popup, typeof(Popup))]
[TemplatePart(PART_Presenter, typeof(TimePickerPresenter))]
[TemplatePart(PART_Button, typeof(Button))]
public class TimePicker : TemplatedControl, IClearControl, IInnerContentControl, IPopupInnerContent
{
public const string PART_TextBox = "PART_TextBox";
public const string PART_Popup = "PART_Popup";
public const string PART_Presenter = "PART_Presenter";
public const string PART_Button = "PART_Button";
public static readonly StyledProperty<string?> DisplayFormatProperty =
AvaloniaProperty.Register<TimePicker, string?>(
nameof(DisplayFormat), "HH:mm:ss");
public static readonly StyledProperty<string> PanelFormatProperty = AvaloniaProperty.Register<TimePicker, string>(
nameof(PanelFormat), "HH mm ss");
public static readonly StyledProperty<TimeSpan?> SelectedTimeProperty =
AvaloniaProperty.Register<TimePicker, TimeSpan?>(
nameof(SelectedTime));
public static readonly StyledProperty<bool> NeedConfirmationProperty = AvaloniaProperty.Register<TimePicker, bool>(
nameof(NeedConfirmation));
public static readonly StyledProperty<object?> InnerLeftContentProperty =
AvaloniaProperty.Register<TimePicker, object?>(
nameof(InnerLeftContent));
public static readonly StyledProperty<object?> InnerRightContentProperty =
AvaloniaProperty.Register<TimePicker, object?>(
nameof(InnerRightContent));
public static readonly StyledProperty<object?> PopupInnerTopContentProperty =
AvaloniaProperty.Register<TimePicker, object?>(
nameof(PopupInnerTopContent));
public static readonly StyledProperty<object?> PopupInnerBottomContentProperty =
AvaloniaProperty.Register<TimePicker, object?>(
nameof(PopupInnerBottomContent));
public static readonly StyledProperty<string?> WatermarkProperty = AvaloniaProperty.Register<TimePicker, string?>(
nameof(Watermark));
public static readonly StyledProperty<bool> IsDropdownOpenProperty = AvaloniaProperty.Register<TimePicker, bool>(
nameof(IsDropdownOpen), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<bool> IsReadonlyProperty = AvaloniaProperty.Register<TimePicker, bool>(
nameof(IsReadonly));
private Button? _button;
private Popup? _popup;
private TimePickerPresenter? _presenter;
private TextBox? _textBox;
static TimePicker()
{
SelectedTimeProperty.Changed.AddClassHandler<TimePicker, TimeSpan?>((picker, args) =>
picker.OnSelectionChanged(args));
}
public bool IsReadonly
{
get => GetValue(IsReadonlyProperty);
set => SetValue(IsReadonlyProperty, value);
}
public bool IsDropdownOpen
{
get => GetValue(IsDropdownOpenProperty);
set => SetValue(IsDropdownOpenProperty, value);
}
public string? Watermark
{
get => GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
public string? DisplayFormat
{
get => GetValue(DisplayFormatProperty);
set => SetValue(DisplayFormatProperty, value);
}
public string PanelFormat
{
get => GetValue(PanelFormatProperty);
set => SetValue(PanelFormatProperty, value);
}
public TimeSpan? SelectedTime
{
get => GetValue(SelectedTimeProperty);
set => SetValue(SelectedTimeProperty, value);
}
public bool NeedConfirmation
{
get => GetValue(NeedConfirmationProperty);
set => SetValue(NeedConfirmationProperty, value);
}
public void Clear()
{
Focus(NavigationMethod.Pointer);
_presenter?.SetValue(TimePickerPresenter.TimeProperty, null);
}
public object? InnerLeftContent
{
get => GetValue(InnerLeftContentProperty);
set => SetValue(InnerLeftContentProperty, value);
}
public object? InnerRightContent
{
get => GetValue(InnerRightContentProperty);
set => SetValue(InnerRightContentProperty, value);
}
public object? PopupInnerTopContent
{
get => GetValue(PopupInnerTopContentProperty);
set => SetValue(PopupInnerTopContentProperty, value);
}
public object? PopupInnerBottomContent
{
get => GetValue(PopupInnerBottomContentProperty);
set => SetValue(PopupInnerBottomContentProperty, value);
}
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);
_textBox = e.NameScope.Find<TextBox>(PART_TextBox);
_popup = e.NameScope.Find<Popup>(PART_Popup);
_presenter = e.NameScope.Find<TimePickerPresenter>(PART_Presenter);
_button = e.NameScope.Find<Button>(PART_Button);
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.AddHandler(OnTextChanged, _textBox);
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox);
Button.ClickEvent.AddHandler(OnButtonClick, _button);
SetCurrentValue(SelectedTimeProperty, DateTime.Now.TimeOfDay);
}
private void OnButtonClick(object? sender, RoutedEventArgs e)
{
Focus(NavigationMethod.Pointer);
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
}
private void OnTextBoxPointerPressed(object? sender, PointerPressedEventArgs e)
{
SetCurrentValue(IsDropdownOpenProperty, true);
}
private void OnTextBoxGetFocus(object? sender, GotFocusEventArgs e)
{
IsDropdownOpen = true;
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
SetCurrentValue(IsDropdownOpenProperty, false);
e.Handled = true;
return;
}
if (e.Key == Key.Down)
{
SetCurrentValue(IsDropdownOpenProperty, true);
e.Handled = true;
return;
}
if (e.Key == Key.Tab)
{
SetCurrentValue(IsDropdownOpenProperty, false);
return;
}
base.OnKeyDown(e);
}
private void OnTextChanged(object? sender, TextChangedEventArgs e)
{
if (string.IsNullOrEmpty(_textBox?.Text))
{
TimePickerPresenter.TimeProperty.SetValue(null, _presenter);
}
else if (DisplayFormat is null || DisplayFormat.Length == 0)
{
if (TimeSpan.TryParse(_textBox?.Text, out var defaultTime))
TimePickerPresenter.TimeProperty.SetValue(defaultTime, _presenter);
}
else
{
if (DateTime.TryParseExact(_textBox?.Text, DisplayFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None,
out var time)) TimePickerPresenter.TimeProperty.SetValue(time.TimeOfDay, _presenter);
}
}
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<TimeSpan?> args)
{
if (_textBox is null) return;
var time = args.NewValue.Value;
if (time is null)
{
_textBox.Text = null;
return;
}
var date = new DateTime(1, 1, 1, time.Value.Hours, time.Value.Minutes, time.Value.Seconds);
var text = date.ToString(DisplayFormat);
_textBox.Text = text;
}
public void Confirm()
{
_presenter?.Confirm();
SetCurrentValue(IsDropdownOpenProperty, false);
TopLevel.GetTopLevel(this);
Focus();
}
public void Dismiss()
{
SetCurrentValue(IsDropdownOpenProperty, false);
Focus();
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error)
{
base.UpdateDataValidation(property, state, error);
if (property == SelectedTimeProperty) DataValidationErrors.SetError(this, error);
}
}

View File

@@ -0,0 +1,318 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls;
[TemplatePart(PART_PickerContainer, typeof(Grid))]
[TemplatePart(PART_HourSelector, typeof(DateTimePickerPanel))]
[TemplatePart(PART_MinuteSelector, typeof(DateTimePickerPanel))]
[TemplatePart(PART_SecondSelector, typeof(DateTimePickerPanel))]
[TemplatePart(PART_AmPmSelector, typeof(DateTimePickerPanel))]
[TemplatePart(PART_HourScrollPanel, typeof(Control))]
[TemplatePart(PART_MinuteScrollPanel, typeof(Control))]
[TemplatePart(PART_SecondScrollPanel, typeof(Control))]
[TemplatePart(PART_AmPmScrollPanel, typeof(Control))]
[TemplatePart(PART_FirstSeparator, typeof(Control))]
[TemplatePart(PART_SecondSeparator, typeof(Control))]
[TemplatePart(PART_ThirdSeparator, typeof(Control))]
public class TimePickerPresenter : TemplatedControl
{
public const string PART_HourSelector = "PART_HourSelector";
public const string PART_MinuteSelector = "PART_MinuteSelector";
public const string PART_SecondSelector = "PART_SecondSelector";
public const string PART_AmPmSelector = "PART_AmPmSelector";
public const string PART_PickerContainer = "PART_PickerContainer";
public const string PART_HourScrollPanel = "PART_HourScrollPanel";
public const string PART_MinuteScrollPanel = "PART_MinuteScrollPanel";
public const string PART_SecondScrollPanel = "PART_SecondScrollPanel";
public const string PART_AmPmScrollPanel = "PART_AmPmScrollPanel";
public const string PART_FirstSeparator = "PART_FirstSeparator";
public const string PART_SecondSeparator = "PART_SecondSeparator";
public const string PART_ThirdSeparator = "PART_ThirdSeparator";
public static readonly StyledProperty<bool> NeedsConfirmationProperty =
AvaloniaProperty.Register<TimePickerPresenter, bool>(
nameof(NeedsConfirmation));
public static readonly StyledProperty<int> MinuteIncrementProperty =
AvaloniaProperty.Register<TimePickerPresenter, int>(
nameof(MinuteIncrement));
public static readonly StyledProperty<int> SecondIncrementProperty =
AvaloniaProperty.Register<TimePickerPresenter, int>(
nameof(SecondIncrement));
public static readonly StyledProperty<TimeSpan?> TimeProperty =
AvaloniaProperty.Register<TimePickerPresenter, TimeSpan?>(
nameof(Time));
public static readonly StyledProperty<string> PanelFormatProperty =
AvaloniaProperty.Register<TimePickerPresenter, string>(
nameof(PanelFormat), "HH mm ss t");
private Control? _ampmScrollPanel;
private DateTimePickerPanel? _ampmSelector;
private Control? _firstSeparator;
private Control? _hourScrollPanel;
private DateTimePickerPanel? _hourSelector;
private Control? _minuteScrollPanel;
private DateTimePickerPanel? _minuteSelector;
private Grid? _pickerContainer;
private Control? _secondScrollPanel;
private DateTimePickerPanel? _secondSelector;
private Control? _secondSeparator;
private Control? _thirdSeparator;
internal TimeSpan _timeHolder;
private bool _updateFromTimeChange;
private bool _use12Clock;
static TimePickerPresenter()
{
PanelFormatProperty.Changed.AddClassHandler<TimePickerPresenter, string>((presenter, args) =>
presenter.OnPanelFormatChanged(args));
TimeProperty.Changed.AddClassHandler<TimePickerPresenter, TimeSpan?>((presenter, args) =>
presenter.OnTimeChanged(args));
}
public TimePickerPresenter()
{
SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay);
}
public bool NeedsConfirmation
{
get => GetValue(NeedsConfirmationProperty);
set => SetValue(NeedsConfirmationProperty, value);
}
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 string PanelFormat
{
get => GetValue(PanelFormatProperty);
set => SetValue(PanelFormatProperty, value);
}
public event EventHandler<TimePickerSelectedValueChangedEventArgs>? SelectedTimeChanged;
private void OnTimeChanged(AvaloniaPropertyChangedEventArgs<TimeSpan?> args)
{
_updateFromTimeChange = true;
UpdatePanelsFromSelectedTime(args.NewValue.Value);
_updateFromTimeChange = false;
SelectedTimeChanged?.Invoke(this,
new TimePickerSelectedValueChangedEventArgs(args.OldValue.Value, args.NewValue.Value));
}
private void OnPanelFormatChanged(AvaloniaPropertyChangedEventArgs<string> args)
{
var format = args.NewValue.Value;
UpdatePanelLayout(format);
}
private void UpdatePanelLayout(string panelFormat)
{
var parts = panelFormat.Split(new[] { ' ', '-', ':' }, StringSplitOptions.RemoveEmptyEntries);
var panels = new List<Control?>();
foreach (var part in parts)
{
if (part.Length < 1) continue;
if ((part.Contains('h') || part.Contains('H')) && !panels.Contains(_hourScrollPanel))
{
panels.Add(_hourScrollPanel);
_use12Clock = part.Contains('h');
_hourSelector?.SetValue(DateTimePickerPanel.ItemFormatProperty, part.ToLower());
if (_hourSelector is not null)
{
_hourSelector.MaximumValue = _use12Clock ? 12 : 23;
_hourSelector.MinimumValue = _use12Clock ? 1: 0;
}
}
else if (part[0] == 'm' && !panels.Contains(_minuteSelector))
{
panels.Add(_minuteScrollPanel);
_minuteSelector?.SetValue(DateTimePickerPanel.ItemFormatProperty, part);
}
else if (part[0] == 's' && !panels.Contains(_secondScrollPanel))
{
panels.Add(_secondScrollPanel);
_secondSelector?.SetValue(DateTimePickerPanel.ItemFormatProperty, part.Replace('s', 'm'));
}
else if (part[0] == 't' && !panels.Contains(_ampmScrollPanel))
{
panels.Add(_ampmScrollPanel);
_ampmSelector?.SetValue(DateTimePickerPanel.ItemFormatProperty, part);
}
}
if (panels.Count < 1) return;
IsVisibleProperty.SetValue(false, _hourScrollPanel, _minuteScrollPanel, _secondScrollPanel, _ampmScrollPanel,
_firstSeparator, _secondSeparator, _thirdSeparator);
for (var i = 0; i < panels.Count; i++)
{
var panel = panels[i];
if (panel is null) continue;
panel.IsVisible = true;
Grid.SetColumn(panel, 2 * i);
var separator = i switch
{
0 => _firstSeparator,
1 => _secondSeparator,
2 => _thirdSeparator,
_ => null
};
if (i != panels.Count - 1) IsVisibleProperty.SetValue(true, separator);
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (_hourSelector is not null) _hourSelector.SelectionChanged -= OnPanelSelectionChanged;
if (_minuteSelector is not null) _minuteSelector.SelectionChanged -= OnPanelSelectionChanged;
if (_secondSelector is not null) _secondSelector.SelectionChanged -= OnPanelSelectionChanged;
if (_ampmSelector is not null) _ampmSelector.SelectionChanged -= OnPanelSelectionChanged;
_hourSelector = e.NameScope.Find<DateTimePickerPanel>(PART_HourSelector);
_minuteSelector = e.NameScope.Find<DateTimePickerPanel>(PART_MinuteSelector);
_secondSelector = e.NameScope.Find<DateTimePickerPanel>(PART_SecondSelector);
_ampmSelector = e.NameScope.Find<DateTimePickerPanel>(PART_AmPmSelector);
if (_hourSelector is not null) _hourSelector.SelectionChanged += OnPanelSelectionChanged;
if (_minuteSelector is not null) _minuteSelector.SelectionChanged += OnPanelSelectionChanged;
if (_secondSelector is not null) _secondSelector.SelectionChanged += OnPanelSelectionChanged;
if (_ampmSelector is not null) _ampmSelector.SelectionChanged += OnPanelSelectionChanged;
_pickerContainer = e.NameScope.Find<Grid>(PART_PickerContainer);
_hourScrollPanel = e.NameScope.Find<Control>(PART_HourScrollPanel);
_minuteScrollPanel = e.NameScope.Find<Control>(PART_MinuteScrollPanel);
_secondScrollPanel = e.NameScope.Find<Control>(PART_SecondScrollPanel);
_ampmScrollPanel = e.NameScope.Find<Control>(PART_AmPmScrollPanel);
_firstSeparator = e.NameScope.Find<Control>(PART_FirstSeparator);
_secondSeparator = e.NameScope.Find<Control>(PART_SecondSeparator);
_thirdSeparator = e.NameScope.Find<Control>(PART_ThirdSeparator);
Initialize();
UpdatePanelLayout(PanelFormat);
UpdatePanelsFromSelectedTime(Time);
}
private void OnPanelSelectionChanged(object sender, System.EventArgs e)
{
if (_updateFromTimeChange) return;
if (!_use12Clock && sender == _ampmSelector) return;
var time = NeedsConfirmation ? _timeHolder : Time ?? DateTime.Now.TimeOfDay;
var hour = _hourSelector?.SelectedValue ?? time.Hours;
var minute = _minuteSelector?.SelectedValue ?? time.Minutes;
var second = _secondSelector?.SelectedValue ?? time.Seconds;
var ampm = _ampmSelector?.SelectedValue ?? (time.Hours >= 12 ? 1 : 0);
if (_use12Clock)
hour = ampm switch
{
0 when hour == 12 => 0,
1 when hour < 12 => hour + 12,
_ => hour
};
else
{
ampm = hour switch
{
>= 12 => 1,
_ => 0
};
SetIfChanged(_ampmSelector, ampm);
}
var newTime = new TimeSpan(hour, minute, second);
if (NeedsConfirmation)
_timeHolder = newTime;
else
SetCurrentValue(TimeProperty, newTime);
}
private void UpdatePanelsFromSelectedTime(TimeSpan? time)
{
if (time is null) return;
if (_hourSelector is not null)
{
var index = _use12Clock ? time.Value.Hours % 12 : time.Value.Hours;
if (_use12Clock && index == 0) index = 12;
SetIfChanged(_hourSelector, index);
}
SetIfChanged(_minuteSelector, time.Value.Minutes);
SetIfChanged(_secondSelector, time.Value.Seconds);
var ampm = time.Value.Hours switch
{
>= 12 => 1,
_ => 0
};
SetIfChanged(_ampmSelector, ampm);
if (_ampmSelector is not null)
{
_ampmSelector.IsEnabled = _use12Clock;
}
}
private void Initialize()
{
if (_pickerContainer is null) return;
if (_hourSelector is not null)
{
_hourSelector.ItemFormat = "hh";
_hourSelector.MaximumValue = _use12Clock ? 12 : 23;
_hourSelector.MinimumValue = _use12Clock ? 1 : 0;
}
if (_minuteSelector is not null)
{
_minuteSelector.ItemFormat = "mm";
_minuteSelector.MaximumValue = 59;
_minuteSelector.MinimumValue = 0;
}
if (_secondSelector is not null)
{
_secondSelector.ItemFormat = "mm";
_secondSelector.MaximumValue = 59;
_secondSelector.MinimumValue = 0;
}
if (_ampmSelector is not null)
{
_ampmSelector.ItemFormat = "t";
_ampmSelector.MaximumValue = 1;
_ampmSelector.MinimumValue = 0;
}
}
public void Confirm()
{
if (NeedsConfirmation) SetCurrentValue(TimeProperty, _timeHolder);
}
private void SetIfChanged(DateTimePickerPanel? panel, int index)
{
if (panel is null) return;
if (panel.SelectedValue != index) panel.SelectedValue = index;
}
}

View File

@@ -0,0 +1,29 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Ursa.Controls;
public class UrsaDateTimeScrollPanel : DateTimePickerPanel
{
protected override Size MeasureOverride(Size availableSize)
{
var size = base.MeasureOverride(availableSize);
var width = this.Children.Select(a=>a.DesiredSize.Width).Max();
width = Math.Max(width, this.MinWidth);
return new Size(width, size.Height);
}
protected override Size ArrangeOverride(Size finalSize)
{
var width = this.Children.Select(a=>a.DesiredSize.Width).Max();
width = Math.Max(width, this.MinWidth);
finalSize = new Size(width, finalSize.Height);
return base.ArrangeOverride(finalSize);
}
}

View File

@@ -21,8 +21,11 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Controls\Panels\"/>
<None Include="irihi.png" Pack="true" PackagePath=""/>
</ItemGroup>
<ItemGroup>
<Folder Include="Controls\Panels\" />
</ItemGroup>
</Project>