From 237bc0beb82ef408b442e2e982df87d4c6f1824c Mon Sep 17 00:00:00 2001 From: rabbitism Date: Wed, 24 Apr 2024 21:28:34 +0800 Subject: [PATCH 01/18] feat: initialize. --- src/Ursa/Controls/TimePicker/TimePicker.cs | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 src/Ursa/Controls/TimePicker/TimePicker.cs diff --git a/src/Ursa/Controls/TimePicker/TimePicker.cs b/src/Ursa/Controls/TimePicker/TimePicker.cs new file mode 100644 index 0000000..bbb3729 --- /dev/null +++ b/src/Ursa/Controls/TimePicker/TimePicker.cs @@ -0,0 +1,35 @@ +using Avalonia; +using Avalonia.Controls.Primitives; + +namespace Ursa.Controls.TimePicker; + +public class TimePicker: TemplatedControl +{ + public static readonly StyledProperty DisplayFormatProperty = AvaloniaProperty.Register( + nameof(DisplayFormat), defaultValue:"HH:mm:ss"); + + public string DisplayFormat + { + get => GetValue(DisplayFormatProperty); + set => SetValue(DisplayFormatProperty, value); + } + + public static readonly StyledProperty PanelPlacementProperty = + AvaloniaProperty.Register( + nameof(PanelPlacement), defaultValue: "HH mm ss"); + + public string PanelPlacement + { + get => GetValue(PanelPlacementProperty); + set => SetValue(PanelPlacementProperty, value); + } + + public static readonly StyledProperty SelectedTimeProperty = AvaloniaProperty.Register( + nameof(SelectedTime)); + + public TimeSpan? SelectedTime + { + get => GetValue(SelectedTimeProperty); + set => SetValue(SelectedTimeProperty, value); + } +} \ No newline at end of file From 294d066bbdc2acc1dd96481d6f7b88b0adf8c839 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Wed, 24 Apr 2024 23:17:49 +0800 Subject: [PATCH 02/18] feat: initialize panel. --- .../Controls/Panels/UrsaTimePickerPanel.cs | 28 +++++++++++++++++++ src/Ursa/Controls/TimePicker/TimePicker.cs | 9 ++++++ src/Ursa/Ursa.csproj | 1 - 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/Ursa/Controls/Panels/UrsaTimePickerPanel.cs diff --git a/src/Ursa/Controls/Panels/UrsaTimePickerPanel.cs b/src/Ursa/Controls/Panels/UrsaTimePickerPanel.cs new file mode 100644 index 0000000..d784d0c --- /dev/null +++ b/src/Ursa/Controls/Panels/UrsaTimePickerPanel.cs @@ -0,0 +1,28 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; + +namespace Ursa.Controls.Panels; + +/// +/// The panel to display items for time selection +/// +public class UrsaTimePickerPanel: Panel +{ + /// + /// Defines whether the panel is looping. + /// This is ont applicable for columns like year and AM/PM designation. + /// + public static readonly StyledProperty IsLoopingProperty = AvaloniaProperty.Register( + nameof(IsLooping)); + + /// + /// Gets or sets the value of . + /// + public bool IsLooping + { + get => GetValue(IsLoopingProperty); + set => SetValue(IsLoopingProperty, value); + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/TimePicker/TimePicker.cs b/src/Ursa/Controls/TimePicker/TimePicker.cs index bbb3729..d1d5a0a 100644 --- a/src/Ursa/Controls/TimePicker/TimePicker.cs +++ b/src/Ursa/Controls/TimePicker/TimePicker.cs @@ -32,4 +32,13 @@ public class TimePicker: TemplatedControl get => GetValue(SelectedTimeProperty); set => SetValue(SelectedTimeProperty, value); } + + public static readonly StyledProperty NeedConfirmProperty = AvaloniaProperty.Register( + nameof(NeedConfirm)); + + public bool NeedConfirm + { + get => GetValue(NeedConfirmProperty); + set => SetValue(NeedConfirmProperty, value); + } } \ No newline at end of file diff --git a/src/Ursa/Ursa.csproj b/src/Ursa/Ursa.csproj index 6b5f3e0..d56963a 100644 --- a/src/Ursa/Ursa.csproj +++ b/src/Ursa/Ursa.csproj @@ -21,7 +21,6 @@ - From 0fc5cc563755d9c363bf8b6f21e9ba35834dee47 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 25 Apr 2024 19:29:39 +0800 Subject: [PATCH 03/18] feat: logical scrollable. --- .../Controls/DateTimePicker/TimePicker.cs | 110 ++++++++++++++++++ .../DateTimePicker/UrsaTimePickerPanel.cs | 62 ++++++++++ .../Controls/Panels/UrsaTimePickerPanel.cs | 28 ----- src/Ursa/Controls/TimePicker/TimePicker.cs | 44 ------- src/Ursa/Ursa.csproj | 4 + 5 files changed, 176 insertions(+), 72 deletions(-) create mode 100644 src/Ursa/Controls/DateTimePicker/TimePicker.cs create mode 100644 src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs delete mode 100644 src/Ursa/Controls/Panels/UrsaTimePickerPanel.cs delete mode 100644 src/Ursa/Controls/TimePicker/TimePicker.cs diff --git a/src/Ursa/Controls/DateTimePicker/TimePicker.cs b/src/Ursa/Controls/DateTimePicker/TimePicker.cs new file mode 100644 index 0000000..6650a24 --- /dev/null +++ b/src/Ursa/Controls/DateTimePicker/TimePicker.cs @@ -0,0 +1,110 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Irihi.Avalonia.Shared.Contracts; + +namespace Ursa.Controls.TimePicker; + +public class TimePicker: TemplatedControl, IClearControl +{ + private TimeSpan? _selectedTimeHolder; + + public static readonly StyledProperty DisplayFormatProperty = AvaloniaProperty.Register( + nameof(DisplayFormat), defaultValue: "HH:mm:ss"); + + public string DisplayFormat + { + get => GetValue(DisplayFormatProperty); + set => SetValue(DisplayFormatProperty, value); + } + + public static readonly StyledProperty PanelFormatProperty = AvaloniaProperty.Register( + nameof(PanelFormat), defaultValue: "HH mm ss"); + + public string PanelFormat + { + get => GetValue(PanelFormatProperty); + set => SetValue(PanelFormatProperty, value); + } + + public static readonly StyledProperty SelectedTimeProperty = AvaloniaProperty.Register( + nameof(SelectedTime)); + + public TimeSpan? SelectedTime + { + get => GetValue(SelectedTimeProperty); + set => SetValue(SelectedTimeProperty, value); + } + + public static readonly StyledProperty NeedConfirmationProperty = AvaloniaProperty.Register( + nameof(NeedConfirmation)); + + public bool NeedConfirmation + { + get => GetValue(NeedConfirmationProperty); + set => SetValue(NeedConfirmationProperty, value); + } + + public static readonly StyledProperty IsLoopingProperty = AvaloniaProperty.Register( + nameof(IsLooping)); + + public bool IsLooping + { + get => GetValue(IsLoopingProperty); + set => SetValue(IsLoopingProperty, value); + } + + static TimePicker() + { + PanelFormatProperty.Changed.AddClassHandler((picker, args)=> picker.OnPanelFormatChanged(args)); + } + + private void OnPanelFormatChanged(AvaloniaPropertyChangedEventArgs args) + { + var format = args.NewValue.Value; + string[] parts = format.Split(new char[] { ' ', '-', ':' }); + } + + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + + } + + private void OnSelectionChanged() + { + if (NeedConfirmation) + { + _selectedTimeHolder = new TimeSpan(); + } + else + { + SelectedTime = new TimeSpan(); + } + } + + public void Clear() + { + SetCurrentValue(SelectedTimeProperty, null); + } + + public void Confirm() + { + if (NeedConfirmation) + { + // TODO: close popup. + SetCurrentValue(SelectedTimeProperty, _selectedTimeHolder); + } + } + + protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) + { + base.UpdateDataValidation(property, state, error); + if (property == SelectedTimeProperty) + { + DataValidationErrors.SetError(this, error); + } + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs b/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs new file mode 100644 index 0000000..5f8397c --- /dev/null +++ b/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs @@ -0,0 +1,62 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; + +namespace Ursa.Controls.Panels; + +/// +/// The panel to display items for time selection +/// +public class UrsaTimePickerPanel: Panel, ILogicalScrollable +{ + public static readonly StyledProperty ItemHeightProperty = + AvaloniaProperty.Register( + nameof(ItemHeight), defaultValue: 32); + + public double ItemHeight + { + get => GetValue(ItemHeightProperty); + set => SetValue(ItemHeightProperty, value); + } + + public static readonly StyledProperty ShouldLoopProperty = AvaloniaProperty.Register( + nameof(ShouldLoop)); + + public bool ShouldLoop + { + get => GetValue(ShouldLoopProperty); + set => SetValue(ShouldLoopProperty, value); + } + + static UrsaTimePickerPanel() + { + ItemHeightProperty.Changed.AddClassHandler((panel, args) => panel.OnItemHeightChanged(args)); + AffectsArrange(ItemHeightProperty); + AffectsMeasure(ItemHeightProperty); + } + + private Size _scrollSize; + private Size _pageScrollSize; + private void OnItemHeightChanged(AvaloniaPropertyChangedEventArgs args) + { + var newValue = args.NewValue.Value; + _scrollSize = new Size(0, newValue); + _pageScrollSize = new Size(0, newValue * 3); + } + + public event EventHandler? OnSelectionChanged; + public Size Extent { get; private set; } + public Vector Offset { get; set; } + public Size Viewport => Bounds.Size; + public bool BringIntoView(Control target, Rect targetRect) => false; + public Control? GetControlInDirection(NavigationDirection direction, Control? from) => null; + public void RaiseScrollInvalidated(System.EventArgs e) => ScrollInvalidated?.Invoke(this, e); + public bool CanHorizontallyScroll { get => false; set { } } + public bool CanVerticallyScroll { get => false; set {} } + public bool IsLogicalScrollEnabled => true; + public Size ScrollSize => _scrollSize; + public Size PageScrollSize => _pageScrollSize; + public event EventHandler? ScrollInvalidated; +} \ No newline at end of file diff --git a/src/Ursa/Controls/Panels/UrsaTimePickerPanel.cs b/src/Ursa/Controls/Panels/UrsaTimePickerPanel.cs deleted file mode 100644 index d784d0c..0000000 --- a/src/Ursa/Controls/Panels/UrsaTimePickerPanel.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Controls.Primitives; -using Avalonia.Input; - -namespace Ursa.Controls.Panels; - -/// -/// The panel to display items for time selection -/// -public class UrsaTimePickerPanel: Panel -{ - /// - /// Defines whether the panel is looping. - /// This is ont applicable for columns like year and AM/PM designation. - /// - public static readonly StyledProperty IsLoopingProperty = AvaloniaProperty.Register( - nameof(IsLooping)); - - /// - /// Gets or sets the value of . - /// - public bool IsLooping - { - get => GetValue(IsLoopingProperty); - set => SetValue(IsLoopingProperty, value); - } -} \ No newline at end of file diff --git a/src/Ursa/Controls/TimePicker/TimePicker.cs b/src/Ursa/Controls/TimePicker/TimePicker.cs deleted file mode 100644 index d1d5a0a..0000000 --- a/src/Ursa/Controls/TimePicker/TimePicker.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Avalonia; -using Avalonia.Controls.Primitives; - -namespace Ursa.Controls.TimePicker; - -public class TimePicker: TemplatedControl -{ - public static readonly StyledProperty DisplayFormatProperty = AvaloniaProperty.Register( - nameof(DisplayFormat), defaultValue:"HH:mm:ss"); - - public string DisplayFormat - { - get => GetValue(DisplayFormatProperty); - set => SetValue(DisplayFormatProperty, value); - } - - public static readonly StyledProperty PanelPlacementProperty = - AvaloniaProperty.Register( - nameof(PanelPlacement), defaultValue: "HH mm ss"); - - public string PanelPlacement - { - get => GetValue(PanelPlacementProperty); - set => SetValue(PanelPlacementProperty, value); - } - - public static readonly StyledProperty SelectedTimeProperty = AvaloniaProperty.Register( - nameof(SelectedTime)); - - public TimeSpan? SelectedTime - { - get => GetValue(SelectedTimeProperty); - set => SetValue(SelectedTimeProperty, value); - } - - public static readonly StyledProperty NeedConfirmProperty = AvaloniaProperty.Register( - nameof(NeedConfirm)); - - public bool NeedConfirm - { - get => GetValue(NeedConfirmProperty); - set => SetValue(NeedConfirmProperty, value); - } -} \ No newline at end of file diff --git a/src/Ursa/Ursa.csproj b/src/Ursa/Ursa.csproj index d56963a..d0aa0df 100644 --- a/src/Ursa/Ursa.csproj +++ b/src/Ursa/Ursa.csproj @@ -24,4 +24,8 @@ + + + + From c5e56a0c31092285d8dd38f79bddeefe51f186c1 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Thu, 25 Apr 2024 22:49:03 +0800 Subject: [PATCH 04/18] feat: add panel implementations. --- .../DateTimePicker/UrsaTimePickerPanel.cs | 276 +++++++++++++++++- 1 file changed, 274 insertions(+), 2 deletions(-) diff --git a/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs b/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs index 5f8397c..e38d549 100644 --- a/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs +++ b/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs @@ -1,16 +1,64 @@ 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.Panels; +public enum TimePickerPanelType +{ + Hour, + Minute, + Second, + TimePeriod // AM/PM +} /// /// The panel to display items for time selection /// public class UrsaTimePickerPanel: Panel, ILogicalScrollable { + private ScrollContentPresenter? _parentScroller; + private double _extendOne; + private Vector _offset; + private bool _initialized; + private int _countItemAboveBelowSelected; + + public Vector Offset + { + get => _offset; + set => SetOffset(value); + } + + private int _increment; + + public int Increment + { + get => _increment; + set => _increment = value; + } + + private int _selectedIndex; + public int SelectedIndex + { + get => _selectedIndex; + set + { + _selectedIndex = value; + } + } + + private int _selectedValue; + + public int SelectedValue + { + get => _selectedValue; + set => _selectedValue = value; + } + public static readonly StyledProperty ItemHeightProperty = AvaloniaProperty.Register( nameof(ItemHeight), defaultValue: 32); @@ -46,9 +94,232 @@ public class UrsaTimePickerPanel: Panel, ILogicalScrollable _pageScrollSize = new Size(0, newValue * 3); } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + _parentScroller?.RemoveHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + _parentScroller = this.GetVisualParent() as ScrollContentPresenter; + _parentScroller?.AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + } + + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + _parentScroller?.RemoveHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); + _parentScroller = null; + } + + public override void ApplyTemplate() + { + base.ApplyTemplate(); + AddHandler(TappedEvent, OnItemTapped, RoutingStrategies.Bubble); + } + + private void OnItemTapped(object sender, TappedEventArgs e) + { + if (e.Source is Visual source && GetItemFromSource(source) is { Tag: int tag }) + { + SelectedValue = tag; + e.Handled = true; + } + } + + private ListBoxItem? GetItemFromSource(Visual source) + { + var item = source; + while (item != null && !(item is ListBoxItem)) + { + item = item.GetVisualParent(); + } + return item as ListBoxItem; + } + + protected override Size MeasureOverride(Size availableSize) + { + if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height)) + throw new InvalidOperationException("Panel must have finite height"); + if(_initialized) UpdateHelperInfo(); + double initY = availableSize.Height / 2.0 - ItemHeight / 2.0; + _countItemAboveBelowSelected = (int)Math.Ceiling(initY / ItemHeight); + + var children = Children; + + CreateOrDestroyItems(children); + + for(int i = 0; i< children.Count; i++) + { + children[i].Measure(availableSize); + } + + if (!_initialized) + { + UpdateItems(); + RaiseScrollInvalidated(System.EventArgs.Empty); + _initialized = true; + } + + return availableSize; + } + + protected override Size ArrangeOverride(Size finalSize) + { + if (Children.Count == 0) + { + return base.ArrangeOverride(finalSize); + } + + var itemHeight = ItemHeight; + var children = Children; + Rect rc; + double initY = finalSize.Height / 2.0 - itemHeight / 2.0; + + if (ShouldLoop) + { + var currentSet = Math.Truncate(Offset.Y / _extendOne); + initY += _extendOne * currentSet + (_selectedIndex - _countItemAboveBelowSelected) * ItemHeight; + foreach (var child in children) + { + rc = new Rect(0, initY-Offset.Y, finalSize.Width, itemHeight); + child.Arrange(rc); + initY += itemHeight; + } + } + else + { + var first = Math.Max(0, _selectedIndex - _countItemAboveBelowSelected); + foreach (var child in children) + { + rc = new Rect(0, initY+first+itemHeight-Offset.Y, finalSize.Width, itemHeight); + child.Arrange(rc); + initY += itemHeight; + } + } + return finalSize; + } + + private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e) + { + var snapY = Math.Round(Offset.Y / ItemHeight) * ItemHeight; + if(!snapY.Equals(Offset.Y)) + { + Offset = Offset.WithY(snapY); + } + } + + private void SetOffset(Vector value) + { + var oldValue = _offset; + _offset = value; + var dy = _offset.Y - oldValue.Y; + var children = this.Children; + // TODO + } + + private int _range; + private int _maximumValue; + private int _minimumValue; + private int _totalItems; + private void UpdateHelperInfo() + { + _range = _maximumValue - _minimumValue + 1; + _totalItems = (int)Math.Ceiling((double)_range / _increment); + var itemHeight = ItemHeight; + _extent = new Size(0, ShouldLoop ? _totalItems * itemHeight * 100 : _totalItems * itemHeight); + + _extendOne = _totalItems * itemHeight; + _offset = new Vector(0, + ShouldLoop ? _extendOne * 50 + _selectedIndex * itemHeight : _selectedIndex * itemHeight); + } + + private void UpdateItems() + { + var children = Children; + var min = _minimumValue; + var max = _maximumValue; + var selected = SelectedValue; + + int first; + if (ShouldLoop) + { + first = (_selectedIndex - _countItemAboveBelowSelected) % _totalItems; + first = first < 0 ? min + (first + _totalItems) * Increment : min + first * Increment; + } + else + { + first = min + Math.Max(0, _selectedIndex - _countItemAboveBelowSelected) * Increment; + } + + for (int i = 0; i < children.Count; i++) + { + ListBoxItem item = (ListBoxItem)children[i]; + item.Content = first + i * Increment; // TODO + item.Tag = first; + item.IsSelected = first == selected; + first += Increment; + if(first > max) + { + first = min; + } + } + + } + + private void CreateOrDestroyItems(Avalonia.Controls.Controls children) + { + int totalItemsInViewport = _countItemAboveBelowSelected * 2 + 1; + if (!ShouldLoop) + { + int numItemAboveSelect = _countItemAboveBelowSelected; + if (_selectedIndex - _countItemAboveBelowSelected < 0) + { + numItemAboveSelect = _selectedIndex; + } + int numItemBelowSelect = _countItemAboveBelowSelected; + if (_selectedIndex + _countItemAboveBelowSelected >= _totalItems) + { + numItemBelowSelect = _totalItems - _selectedIndex - 1; + } + totalItemsInViewport = numItemAboveSelect + numItemBelowSelect + 1; + } + + while (children.Count totalItemsInViewport) + { + var countToRemove = children.Count - totalItemsInViewport; + children.RemoveRange(children.Count-countToRemove, countToRemove); + } + } + + private int CoerceSelected(int newValue) + { + if(newValue < _minimumValue) + { + return _minimumValue; + } + if(newValue > _maximumValue) + { + return _maximumValue; + } + if (newValue % _increment == 0) return newValue; + var items = Enumerable.Range(_minimumValue, _range).Where(x => x % _increment == 0).ToList(); + var nearest = items.Aggregate((x, y) => Math.Abs(x - newValue) > Math.Abs(y - newValue) ? y : x); + return items.IndexOf(nearest) * Increment; + } public event EventHandler? OnSelectionChanged; - public Size Extent { get; private set; } - public Vector Offset { get; set; } + private Size _extent; + public Size Extent { + get => _extent; + private set => _extent = value; + } public Size Viewport => Bounds.Size; public bool BringIntoView(Control target, Rect targetRect) => false; public Control? GetControlInDirection(NavigationDirection direction, Control? from) => null; @@ -59,4 +330,5 @@ public class UrsaTimePickerPanel: Panel, ILogicalScrollable public Size ScrollSize => _scrollSize; public Size PageScrollSize => _pageScrollSize; public event EventHandler? ScrollInvalidated; + public event EventHandler? SelectionChanged; } \ No newline at end of file From 7f1bd62c90a483d60e6a2daa4e1134426765aa73 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 26 Apr 2024 02:49:58 +0800 Subject: [PATCH 05/18] feat: add demo for presenter temporarily. --- demo/Ursa.Demo/Pages/TimePickerDemo.axaml | 11 + demo/Ursa.Demo/Pages/TimePickerDemo.axaml.cs | 13 + .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 1 + demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 2 + .../ViewModels/TimePickerDemoViewModel.cs | 8 + .../Controls/TimePicker.axaml | 77 +++++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + .../Controls/DateTimePicker/TimePicker.cs | 64 ++-- .../DateTimePicker/TimePickerPresenter.cs | 129 ++++++++ ...kerPanel.cs => UrsaDateTimeScrollPanel.cs} | 282 +++++++++--------- 10 files changed, 412 insertions(+), 176 deletions(-) create mode 100644 demo/Ursa.Demo/Pages/TimePickerDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/TimePickerDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/TimePickerDemoViewModel.cs create mode 100644 src/Ursa.Themes.Semi/Controls/TimePicker.axaml create mode 100644 src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs rename src/Ursa/Controls/DateTimePicker/{UrsaTimePickerPanel.cs => UrsaDateTimeScrollPanel.cs} (57%) diff --git a/demo/Ursa.Demo/Pages/TimePickerDemo.axaml b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml new file mode 100644 index 0000000..d5324f6 --- /dev/null +++ b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml @@ -0,0 +1,11 @@ + + + + + diff --git a/demo/Ursa.Demo/Pages/TimePickerDemo.axaml.cs b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml.cs new file mode 100644 index 0000000..330eb98 --- /dev/null +++ b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml.cs @@ -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(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 80b9f6a..f981fa6 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -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(), diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index a1fcdb7..32066e6 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -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"; diff --git a/demo/Ursa.Demo/ViewModels/TimePickerDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/TimePickerDemoViewModel.cs new file mode 100644 index 0000000..f115429 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/TimePickerDemoViewModel.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public class TimePickerDemoViewModel: ObservableObject +{ + +} \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/TimePicker.axaml b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml new file mode 100644 index 0000000..3f0d93d --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index 6e25af2..0ceb9e3 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -32,6 +32,7 @@ + diff --git a/src/Ursa/Controls/DateTimePicker/TimePicker.cs b/src/Ursa/Controls/DateTimePicker/TimePicker.cs index 6650a24..9cacca4 100644 --- a/src/Ursa/Controls/DateTimePicker/TimePicker.cs +++ b/src/Ursa/Controls/DateTimePicker/TimePicker.cs @@ -4,14 +4,33 @@ using Avalonia.Controls.Primitives; using Avalonia.Data; using Irihi.Avalonia.Shared.Contracts; -namespace Ursa.Controls.TimePicker; +namespace Ursa.Controls; -public class TimePicker: TemplatedControl, IClearControl +public class TimePicker : TemplatedControl, IClearControl { - private TimeSpan? _selectedTimeHolder; - public static readonly StyledProperty DisplayFormatProperty = AvaloniaProperty.Register( - nameof(DisplayFormat), defaultValue: "HH:mm:ss"); + nameof(DisplayFormat), "HH:mm:ss"); + + public static readonly StyledProperty PanelFormatProperty = AvaloniaProperty.Register( + nameof(PanelFormat), "HH mm ss"); + + public static readonly StyledProperty SelectedTimeProperty = + AvaloniaProperty.Register( + nameof(SelectedTime)); + + public static readonly StyledProperty NeedConfirmationProperty = AvaloniaProperty.Register( + nameof(NeedConfirmation)); + + public static readonly StyledProperty IsLoopingProperty = AvaloniaProperty.Register( + nameof(IsLooping)); + + private TimeSpan? _selectedTimeHolder; + + static TimePicker() + { + PanelFormatProperty.Changed.AddClassHandler((picker, args) => + picker.OnPanelFormatChanged(args)); + } public string DisplayFormat { @@ -19,26 +38,17 @@ public class TimePicker: TemplatedControl, IClearControl set => SetValue(DisplayFormatProperty, value); } - public static readonly StyledProperty PanelFormatProperty = AvaloniaProperty.Register( - nameof(PanelFormat), defaultValue: "HH mm ss"); - public string PanelFormat { get => GetValue(PanelFormatProperty); set => SetValue(PanelFormatProperty, value); } - public static readonly StyledProperty SelectedTimeProperty = AvaloniaProperty.Register( - nameof(SelectedTime)); - public TimeSpan? SelectedTime { get => GetValue(SelectedTimeProperty); set => SetValue(SelectedTimeProperty, value); } - - public static readonly StyledProperty NeedConfirmationProperty = AvaloniaProperty.Register( - nameof(NeedConfirmation)); public bool NeedConfirmation { @@ -46,65 +56,47 @@ public class TimePicker: TemplatedControl, IClearControl set => SetValue(NeedConfirmationProperty, value); } - public static readonly StyledProperty IsLoopingProperty = AvaloniaProperty.Register( - nameof(IsLooping)); - public bool IsLooping { get => GetValue(IsLoopingProperty); set => SetValue(IsLoopingProperty, value); } - static TimePicker() + public void Clear() { - PanelFormatProperty.Changed.AddClassHandler((picker, args)=> picker.OnPanelFormatChanged(args)); + SetCurrentValue(SelectedTimeProperty, null); } private void OnPanelFormatChanged(AvaloniaPropertyChangedEventArgs args) { var format = args.NewValue.Value; - string[] parts = format.Split(new char[] { ' ', '-', ':' }); + var parts = format.Split(' ', '-', ':'); } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - } private void OnSelectionChanged() { if (NeedConfirmation) - { _selectedTimeHolder = new TimeSpan(); - } else - { SelectedTime = new TimeSpan(); - } } - public void Clear() - { - SetCurrentValue(SelectedTimeProperty, null); - } - public void Confirm() { if (NeedConfirmation) - { // TODO: close popup. SetCurrentValue(SelectedTimeProperty, _selectedTimeHolder); - } } protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) { base.UpdateDataValidation(property, state, error); - if (property == SelectedTimeProperty) - { - DataValidationErrors.SetError(this, error); - } + if (property == SelectedTimeProperty) DataValidationErrors.SetError(this, error); } } \ No newline at end of file diff --git a/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs b/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs new file mode 100644 index 0000000..ac27c1f --- /dev/null +++ b/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs @@ -0,0 +1,129 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; + +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))] +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"; + + private DateTimePickerPanel? _hourSelector; + private DateTimePickerPanel? _minuteSelector; + private DateTimePickerPanel? _secondSelector; + private DateTimePickerPanel? _ampmSelector; + private Grid? _pickerContainer; + + public static readonly StyledProperty NeedsConfirmationProperty = AvaloniaProperty.Register( + nameof(NeedsConfirmation)); + + public bool NeedsConfirmation + { + get => GetValue(NeedsConfirmationProperty); + set => SetValue(NeedsConfirmationProperty, value); + } + + public static readonly StyledProperty MinuteIncrementProperty = AvaloniaProperty.Register( + nameof(MinuteIncrement)); + + public int MinuteIncrement + { + get => GetValue(MinuteIncrementProperty); + set => SetValue(MinuteIncrementProperty, value); + } + + public static readonly StyledProperty TimeProperty = AvaloniaProperty.Register( + nameof(Time)); + + public TimeSpan? Time + { + get => GetValue(TimeProperty); + set => SetValue(TimeProperty, value); + } + + public static readonly StyledProperty Use12HoursProperty = AvaloniaProperty.Register( + nameof(Use12Hours)); + + public bool Use12Hours + { + get => GetValue(Use12HoursProperty); + set => SetValue(Use12HoursProperty, value); + } + + public static readonly StyledProperty PanelFormatProperty = AvaloniaProperty.Register( + nameof(PanelFormat)); + + public string PanelFormat + { + get => GetValue(PanelFormatProperty); + set => SetValue(PanelFormatProperty, value); + } + + public TimePickerPresenter() + { + SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay); + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _hourSelector = e.NameScope.Find(PART_HourSelector); + _minuteSelector = e.NameScope.Find(PART_MinuteSelector); + _secondSelector = e.NameScope.Find(PART_SecondSelector); + _ampmSelector = e.NameScope.Find(PART_AmPmSelector); + _pickerContainer = e.NameScope.Find(PART_PickerContainer); + Initialize(); + } + + private void Initialize() + { + if (_pickerContainer is null) return; + var use12Clock = Use12Hours; + if (_hourSelector is not null) + { + _hourSelector.MaximumValue = use12Clock ? 12 : 23; + _hourSelector.MinimumValue = use12Clock ? 1 : 0; + _hourSelector.ItemFormat = "%h"; + var hour = Time?.Hours; + _hourSelector.SelectedValue = hour ?? 0; + } + if(_minuteSelector is not null) + { + _minuteSelector.MaximumValue = 59; + _minuteSelector.MinimumValue = 0; + _minuteSelector.ItemFormat = "mm"; + var minute = Time?.Minutes; + _minuteSelector.SelectedValue = minute ?? 0; + } + if(_secondSelector is not null) + { + _secondSelector.MaximumValue = 59; + _secondSelector.MinimumValue = 0; + _secondSelector.ItemFormat = "mm"; + var second = Time?.Seconds; + _secondSelector.SelectedValue = second ?? 0; + } + if(_ampmSelector is not null) + { + _ampmSelector.MaximumValue = 1; + _ampmSelector.MinimumValue = 0; + _ampmSelector.ItemFormat = "%t"; + var ampm = Time?.Hours switch + { + >= 12 => 1, + _ => 0 + }; + _ampmSelector.SelectedValue = ampm; + } + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs b/src/Ursa/Controls/DateTimePicker/UrsaDateTimeScrollPanel.cs similarity index 57% rename from src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs rename to src/Ursa/Controls/DateTimePicker/UrsaDateTimeScrollPanel.cs index e38d549..36a8893 100644 --- a/src/Ursa/Controls/DateTimePicker/UrsaTimePickerPanel.cs +++ b/src/Ursa/Controls/DateTimePicker/UrsaDateTimeScrollPanel.cs @@ -9,59 +9,75 @@ using Avalonia.VisualTree; namespace Ursa.Controls.Panels; -public enum TimePickerPanelType +public enum UrsaDateTimeScrollPanelType { + Year, + Month, + Day, Hour, Minute, Second, TimePeriod // AM/PM } + /// -/// The panel to display items for time selection +/// The panel to display items for time selection /// -public class UrsaTimePickerPanel: Panel, ILogicalScrollable +public class UrsaDateTimeScrollPanel : Panel, ILogicalScrollable { - private ScrollContentPresenter? _parentScroller; - private double _extendOne; - private Vector _offset; - private bool _initialized; - private int _countItemAboveBelowSelected; - - public Vector Offset - { - get => _offset; - set => SetOffset(value); - } - - private int _increment; - - public int Increment - { - get => _increment; - set => _increment = value; - } - - private int _selectedIndex; - public int SelectedIndex - { - get => _selectedIndex; - set - { - _selectedIndex = value; - } - } - - private int _selectedValue; - - public int SelectedValue - { - get => _selectedValue; - set => _selectedValue = value; - } - public static readonly StyledProperty ItemHeightProperty = - AvaloniaProperty.Register( - nameof(ItemHeight), defaultValue: 32); + AvaloniaProperty.Register( + nameof(ItemHeight), 32); + + public static readonly StyledProperty ShouldLoopProperty = + AvaloniaProperty.Register( + nameof(ShouldLoop)); + + public static readonly StyledProperty PanelTypeProperty = AvaloniaProperty.Register( + nameof(PanelType)); + + public UrsaDateTimeScrollPanelType PanelType + { + get => GetValue(PanelTypeProperty); + set => SetValue(PanelTypeProperty, value); + } + + public static readonly StyledProperty ItemFormatProperty = AvaloniaProperty.Register( + nameof(ItemFormat)); + + public string ItemFormat + { + get => GetValue(ItemFormatProperty); + set => SetValue(ItemFormatProperty, value); + } + + + private int _countItemAboveBelowSelected; + private double _extendOne; + + private bool _initialized; + private int _maximumValue; + private int _minimumValue; + private Vector _offset; + private ScrollContentPresenter? _parentScroller; + + private int _range; + + private int _totalItems; + + static UrsaDateTimeScrollPanel() + { + ItemHeightProperty.Changed.AddClassHandler((panel, args) => + panel.OnItemHeightChanged(args)); + AffectsArrange(ItemHeightProperty); + AffectsMeasure(ItemHeightProperty); + } + + public int Increment { get; set; } + + public int SelectedIndex { get; set; } + + public int SelectedValue { get; set; } public double ItemHeight { @@ -69,29 +85,61 @@ public class UrsaTimePickerPanel: Panel, ILogicalScrollable set => SetValue(ItemHeightProperty, value); } - public static readonly StyledProperty ShouldLoopProperty = AvaloniaProperty.Register( - nameof(ShouldLoop)); - public bool ShouldLoop { get => GetValue(ShouldLoopProperty); set => SetValue(ShouldLoopProperty, value); } - - static UrsaTimePickerPanel() + + public Vector Offset { - ItemHeightProperty.Changed.AddClassHandler((panel, args) => panel.OnItemHeightChanged(args)); - AffectsArrange(ItemHeightProperty); - AffectsMeasure(ItemHeightProperty); + get => _offset; + set => SetOffset(value); } - private Size _scrollSize; - private Size _pageScrollSize; + public Size Extent { get; private set; } + + public Size Viewport => Bounds.Size; + + public bool BringIntoView(Control target, Rect targetRect) + { + return false; + } + + public Control? GetControlInDirection(NavigationDirection direction, Control? from) + { + return null; + } + + public void RaiseScrollInvalidated(System.EventArgs e) + { + ScrollInvalidated?.Invoke(this, e); + } + + public bool CanHorizontallyScroll + { + get => false; + set { } + } + + public bool CanVerticallyScroll + { + get => false; + set { } + } + + public bool IsLogicalScrollEnabled => true; + public Size ScrollSize { get; private set; } + + public Size PageScrollSize { get; private set; } + + public event EventHandler? ScrollInvalidated; + private void OnItemHeightChanged(AvaloniaPropertyChangedEventArgs args) { var newValue = args.NewValue.Value; - _scrollSize = new Size(0, newValue); - _pageScrollSize = new Size(0, newValue * 3); + ScrollSize = new Size(0, newValue); + PageScrollSize = new Size(0, newValue * 3); } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) @@ -127,10 +175,7 @@ public class UrsaTimePickerPanel: Panel, ILogicalScrollable private ListBoxItem? GetItemFromSource(Visual source) { var item = source; - while (item != null && !(item is ListBoxItem)) - { - item = item.GetVisualParent(); - } + while (item != null && !(item is ListBoxItem)) item = item.GetVisualParent(); return item as ListBoxItem; } @@ -138,18 +183,15 @@ public class UrsaTimePickerPanel: Panel, ILogicalScrollable { if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height)) throw new InvalidOperationException("Panel must have finite height"); - if(_initialized) UpdateHelperInfo(); - double initY = availableSize.Height / 2.0 - ItemHeight / 2.0; + if (_initialized) UpdateHelperInfo(); + var initY = availableSize.Height / 2.0 - ItemHeight / 2.0; _countItemAboveBelowSelected = (int)Math.Ceiling(initY / ItemHeight); - + var children = Children; - + CreateOrDestroyItems(children); - - for(int i = 0; i< children.Count; i++) - { - children[i].Measure(availableSize); - } + + for (var i = 0; i < children.Count; i++) children[i].Measure(availableSize); if (!_initialized) { @@ -163,72 +205,63 @@ public class UrsaTimePickerPanel: Panel, ILogicalScrollable protected override Size ArrangeOverride(Size finalSize) { - if (Children.Count == 0) - { - return base.ArrangeOverride(finalSize); - } + if (Children.Count == 0) return base.ArrangeOverride(finalSize); var itemHeight = ItemHeight; var children = Children; Rect rc; - double initY = finalSize.Height / 2.0 - itemHeight / 2.0; + var initY = finalSize.Height / 2.0 - itemHeight / 2.0; if (ShouldLoop) { var currentSet = Math.Truncate(Offset.Y / _extendOne); - initY += _extendOne * currentSet + (_selectedIndex - _countItemAboveBelowSelected) * ItemHeight; + initY += _extendOne * currentSet + (SelectedIndex - _countItemAboveBelowSelected) * ItemHeight; foreach (var child in children) { - rc = new Rect(0, initY-Offset.Y, finalSize.Width, itemHeight); + rc = new Rect(0, initY - Offset.Y, finalSize.Width, itemHeight); child.Arrange(rc); initY += itemHeight; } } else { - var first = Math.Max(0, _selectedIndex - _countItemAboveBelowSelected); + var first = Math.Max(0, SelectedIndex - _countItemAboveBelowSelected); foreach (var child in children) { - rc = new Rect(0, initY+first+itemHeight-Offset.Y, finalSize.Width, itemHeight); + rc = new Rect(0, initY + first + itemHeight - Offset.Y, finalSize.Width, itemHeight); child.Arrange(rc); initY += itemHeight; } } + return finalSize; } private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e) { var snapY = Math.Round(Offset.Y / ItemHeight) * ItemHeight; - if(!snapY.Equals(Offset.Y)) - { - Offset = Offset.WithY(snapY); - } + if (!snapY.Equals(Offset.Y)) Offset = Offset.WithY(snapY); } - + private void SetOffset(Vector value) { var oldValue = _offset; _offset = value; var dy = _offset.Y - oldValue.Y; - var children = this.Children; + var children = Children; // TODO } - private int _range; - private int _maximumValue; - private int _minimumValue; - private int _totalItems; private void UpdateHelperInfo() { _range = _maximumValue - _minimumValue + 1; - _totalItems = (int)Math.Ceiling((double)_range / _increment); + _totalItems = (int)Math.Ceiling((double)_range / Increment); var itemHeight = ItemHeight; - _extent = new Size(0, ShouldLoop ? _totalItems * itemHeight * 100 : _totalItems * itemHeight); + Extent = new Size(0, ShouldLoop ? _totalItems * itemHeight * 100 : _totalItems * itemHeight); _extendOne = _totalItems * itemHeight; _offset = new Vector(0, - ShouldLoop ? _extendOne * 50 + _selectedIndex * itemHeight : _selectedIndex * itemHeight); + ShouldLoop ? _extendOne * 50 + SelectedIndex * itemHeight : SelectedIndex * itemHeight); } private void UpdateItems() @@ -241,94 +274,63 @@ public class UrsaTimePickerPanel: Panel, ILogicalScrollable int first; if (ShouldLoop) { - first = (_selectedIndex - _countItemAboveBelowSelected) % _totalItems; + first = (SelectedIndex - _countItemAboveBelowSelected) % _totalItems; first = first < 0 ? min + (first + _totalItems) * Increment : min + first * Increment; } else { - first = min + Math.Max(0, _selectedIndex - _countItemAboveBelowSelected) * Increment; + first = min + Math.Max(0, SelectedIndex - _countItemAboveBelowSelected) * Increment; } - for (int i = 0; i < children.Count; i++) + for (var i = 0; i < children.Count; i++) { - ListBoxItem item = (ListBoxItem)children[i]; + var item = (ListBoxItem)children[i]; item.Content = first + i * Increment; // TODO item.Tag = first; item.IsSelected = first == selected; first += Increment; - if(first > max) - { - first = min; - } + if (first > max) first = min; } - } private void CreateOrDestroyItems(Avalonia.Controls.Controls children) { - int totalItemsInViewport = _countItemAboveBelowSelected * 2 + 1; + var totalItemsInViewport = _countItemAboveBelowSelected * 2 + 1; if (!ShouldLoop) { - int numItemAboveSelect = _countItemAboveBelowSelected; - if (_selectedIndex - _countItemAboveBelowSelected < 0) - { - numItemAboveSelect = _selectedIndex; - } - int numItemBelowSelect = _countItemAboveBelowSelected; - if (_selectedIndex + _countItemAboveBelowSelected >= _totalItems) - { - numItemBelowSelect = _totalItems - _selectedIndex - 1; - } + var numItemAboveSelect = _countItemAboveBelowSelected; + if (SelectedIndex - _countItemAboveBelowSelected < 0) numItemAboveSelect = SelectedIndex; + var numItemBelowSelect = _countItemAboveBelowSelected; + if (SelectedIndex + _countItemAboveBelowSelected >= _totalItems) + numItemBelowSelect = _totalItems - SelectedIndex - 1; totalItemsInViewport = numItemAboveSelect + numItemBelowSelect + 1; } - while (children.Count totalItemsInViewport) { var countToRemove = children.Count - totalItemsInViewport; - children.RemoveRange(children.Count-countToRemove, countToRemove); + children.RemoveRange(children.Count - countToRemove, countToRemove); } } private int CoerceSelected(int newValue) { - if(newValue < _minimumValue) - { - return _minimumValue; - } - if(newValue > _maximumValue) - { - return _maximumValue; - } - if (newValue % _increment == 0) return newValue; - var items = Enumerable.Range(_minimumValue, _range).Where(x => x % _increment == 0).ToList(); + if (newValue < _minimumValue) return _minimumValue; + if (newValue > _maximumValue) return _maximumValue; + if (newValue % Increment == 0) return newValue; + var items = Enumerable.Range(_minimumValue, _range).Where(x => x % Increment == 0).ToList(); var nearest = items.Aggregate((x, y) => Math.Abs(x - newValue) > Math.Abs(y - newValue) ? y : x); return items.IndexOf(nearest) * Increment; } + public event EventHandler? OnSelectionChanged; - private Size _extent; - public Size Extent { - get => _extent; - private set => _extent = value; - } - public Size Viewport => Bounds.Size; - public bool BringIntoView(Control target, Rect targetRect) => false; - public Control? GetControlInDirection(NavigationDirection direction, Control? from) => null; - public void RaiseScrollInvalidated(System.EventArgs e) => ScrollInvalidated?.Invoke(this, e); - public bool CanHorizontallyScroll { get => false; set { } } - public bool CanVerticallyScroll { get => false; set {} } - public bool IsLogicalScrollEnabled => true; - public Size ScrollSize => _scrollSize; - public Size PageScrollSize => _pageScrollSize; - public event EventHandler? ScrollInvalidated; public event EventHandler? SelectionChanged; } \ No newline at end of file From 68545dbf54cbf4e937b17ff4871287f531ebfd6d Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 26 Apr 2024 03:25:24 +0800 Subject: [PATCH 06/18] feat: use new panel. --- .../Controls/TimePicker.axaml | 20 +- .../DateTimePicker/UrsaDateTimeScrollPanel.cs | 327 +----------------- 2 files changed, 21 insertions(+), 326 deletions(-) diff --git a/src/Ursa.Themes.Semi/Controls/TimePicker.axaml b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml index 3f0d93d..38dbfe2 100644 --- a/src/Ursa.Themes.Semi/Controls/TimePicker.axaml +++ b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml @@ -4,7 +4,7 @@ xmlns:u="https://irihi.tech/ursa"> - + @@ -14,20 +14,19 @@ + ColumnDefinitions="*, *, *, *, Auto"> - - - - - -/// The panel to display items for time selection -/// -public class UrsaDateTimeScrollPanel : Panel, ILogicalScrollable -{ - public static readonly StyledProperty ItemHeightProperty = - AvaloniaProperty.Register( - nameof(ItemHeight), 32); - - public static readonly StyledProperty ShouldLoopProperty = - AvaloniaProperty.Register( - nameof(ShouldLoop)); - - public static readonly StyledProperty PanelTypeProperty = AvaloniaProperty.Register( - nameof(PanelType)); - - public UrsaDateTimeScrollPanelType PanelType - { - get => GetValue(PanelTypeProperty); - set => SetValue(PanelTypeProperty, value); - } - - public static readonly StyledProperty ItemFormatProperty = AvaloniaProperty.Register( - nameof(ItemFormat)); - - public string ItemFormat - { - get => GetValue(ItemFormatProperty); - set => SetValue(ItemFormatProperty, value); - } - - - private int _countItemAboveBelowSelected; - private double _extendOne; - - private bool _initialized; - private int _maximumValue; - private int _minimumValue; - private Vector _offset; - private ScrollContentPresenter? _parentScroller; - - private int _range; - - private int _totalItems; - - static UrsaDateTimeScrollPanel() - { - ItemHeightProperty.Changed.AddClassHandler((panel, args) => - panel.OnItemHeightChanged(args)); - AffectsArrange(ItemHeightProperty); - AffectsMeasure(ItemHeightProperty); - } - - public int Increment { get; set; } - - public int SelectedIndex { get; set; } - - public int SelectedValue { get; set; } - - public double ItemHeight - { - get => GetValue(ItemHeightProperty); - set => SetValue(ItemHeightProperty, value); - } - - public bool ShouldLoop - { - get => GetValue(ShouldLoopProperty); - set => SetValue(ShouldLoopProperty, value); - } - - public Vector Offset - { - get => _offset; - set => SetOffset(value); - } - - public Size Extent { get; private set; } - - public Size Viewport => Bounds.Size; - - public bool BringIntoView(Control target, Rect targetRect) - { - return false; - } - - public Control? GetControlInDirection(NavigationDirection direction, Control? from) - { - return null; - } - - public void RaiseScrollInvalidated(System.EventArgs e) - { - ScrollInvalidated?.Invoke(this, e); - } - - public bool CanHorizontallyScroll - { - get => false; - set { } - } - - public bool CanVerticallyScroll - { - get => false; - set { } - } - - public bool IsLogicalScrollEnabled => true; - public Size ScrollSize { get; private set; } - - public Size PageScrollSize { get; private set; } - - public event EventHandler? ScrollInvalidated; - - private void OnItemHeightChanged(AvaloniaPropertyChangedEventArgs args) - { - var newValue = args.NewValue.Value; - ScrollSize = new Size(0, newValue); - PageScrollSize = new Size(0, newValue * 3); - } - - protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnAttachedToVisualTree(e); - _parentScroller?.RemoveHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); - _parentScroller = this.GetVisualParent() as ScrollContentPresenter; - _parentScroller?.AddHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); - } - - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - _parentScroller?.RemoveHandler(Gestures.ScrollGestureEndedEvent, OnScrollGestureEnded); - _parentScroller = null; - } - - public override void ApplyTemplate() - { - base.ApplyTemplate(); - AddHandler(TappedEvent, OnItemTapped, RoutingStrategies.Bubble); - } - - private void OnItemTapped(object sender, TappedEventArgs e) - { - if (e.Source is Visual source && GetItemFromSource(source) is { Tag: int tag }) - { - SelectedValue = tag; - e.Handled = true; - } - } - - private ListBoxItem? GetItemFromSource(Visual source) - { - var item = source; - while (item != null && !(item is ListBoxItem)) item = item.GetVisualParent(); - return item as ListBoxItem; - } - protected override Size MeasureOverride(Size availableSize) { - if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height)) - throw new InvalidOperationException("Panel must have finite height"); - if (_initialized) UpdateHelperInfo(); - var initY = availableSize.Height / 2.0 - ItemHeight / 2.0; - _countItemAboveBelowSelected = (int)Math.Ceiling(initY / ItemHeight); - - var children = Children; - - CreateOrDestroyItems(children); - - for (var i = 0; i < children.Count; i++) children[i].Measure(availableSize); - - if (!_initialized) - { - UpdateItems(); - RaiseScrollInvalidated(System.EventArgs.Empty); - _initialized = true; - } - - return 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) { - if (Children.Count == 0) return base.ArrangeOverride(finalSize); - - var itemHeight = ItemHeight; - var children = Children; - Rect rc; - var initY = finalSize.Height / 2.0 - itemHeight / 2.0; - - if (ShouldLoop) - { - var currentSet = Math.Truncate(Offset.Y / _extendOne); - initY += _extendOne * currentSet + (SelectedIndex - _countItemAboveBelowSelected) * ItemHeight; - foreach (var child in children) - { - rc = new Rect(0, initY - Offset.Y, finalSize.Width, itemHeight); - child.Arrange(rc); - initY += itemHeight; - } - } - else - { - var first = Math.Max(0, SelectedIndex - _countItemAboveBelowSelected); - foreach (var child in children) - { - rc = new Rect(0, initY + first + itemHeight - Offset.Y, finalSize.Width, itemHeight); - child.Arrange(rc); - initY += itemHeight; - } - } - - return 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); } - - private void OnScrollGestureEnded(object sender, ScrollGestureEndedEventArgs e) - { - var snapY = Math.Round(Offset.Y / ItemHeight) * ItemHeight; - if (!snapY.Equals(Offset.Y)) Offset = Offset.WithY(snapY); - } - - private void SetOffset(Vector value) - { - var oldValue = _offset; - _offset = value; - var dy = _offset.Y - oldValue.Y; - var children = Children; - // TODO - } - - private void UpdateHelperInfo() - { - _range = _maximumValue - _minimumValue + 1; - _totalItems = (int)Math.Ceiling((double)_range / Increment); - var itemHeight = ItemHeight; - Extent = new Size(0, ShouldLoop ? _totalItems * itemHeight * 100 : _totalItems * itemHeight); - - _extendOne = _totalItems * itemHeight; - _offset = new Vector(0, - ShouldLoop ? _extendOne * 50 + SelectedIndex * itemHeight : SelectedIndex * itemHeight); - } - - private void UpdateItems() - { - var children = Children; - var min = _minimumValue; - var max = _maximumValue; - var selected = SelectedValue; - - int first; - if (ShouldLoop) - { - first = (SelectedIndex - _countItemAboveBelowSelected) % _totalItems; - first = first < 0 ? min + (first + _totalItems) * Increment : min + first * Increment; - } - else - { - first = min + Math.Max(0, SelectedIndex - _countItemAboveBelowSelected) * Increment; - } - - for (var i = 0; i < children.Count; i++) - { - var item = (ListBoxItem)children[i]; - item.Content = first + i * Increment; // TODO - item.Tag = first; - item.IsSelected = first == selected; - first += Increment; - if (first > max) first = min; - } - } - - private void CreateOrDestroyItems(Avalonia.Controls.Controls children) - { - var totalItemsInViewport = _countItemAboveBelowSelected * 2 + 1; - if (!ShouldLoop) - { - var numItemAboveSelect = _countItemAboveBelowSelected; - if (SelectedIndex - _countItemAboveBelowSelected < 0) numItemAboveSelect = SelectedIndex; - var numItemBelowSelect = _countItemAboveBelowSelected; - if (SelectedIndex + _countItemAboveBelowSelected >= _totalItems) - numItemBelowSelect = _totalItems - SelectedIndex - 1; - totalItemsInViewport = numItemAboveSelect + numItemBelowSelect + 1; - } - - while (children.Count < totalItemsInViewport) - children.Add(new ListBoxItem - { - Height = ItemHeight, - VerticalContentAlignment = VerticalAlignment.Center, - Focusable = false - }); - - if (children.Count > totalItemsInViewport) - { - var countToRemove = children.Count - totalItemsInViewport; - children.RemoveRange(children.Count - countToRemove, countToRemove); - } - } - - private int CoerceSelected(int newValue) - { - if (newValue < _minimumValue) return _minimumValue; - if (newValue > _maximumValue) return _maximumValue; - if (newValue % Increment == 0) return newValue; - var items = Enumerable.Range(_minimumValue, _range).Where(x => x % Increment == 0).ToList(); - var nearest = items.Aggregate((x, y) => Math.Abs(x - newValue) > Math.Abs(y - newValue) ? y : x); - return items.IndexOf(nearest) * Increment; - } - - public event EventHandler? OnSelectionChanged; - public event EventHandler? SelectionChanged; } \ No newline at end of file From 41b530b7bf3f9228bd44b65e3c9415539f4a94a9 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 26 Apr 2024 17:57:53 +0800 Subject: [PATCH 07/18] feat: work around .net standard 2.0 formatting to H --- .../Controls/TimePicker.axaml | 47 +++-- .../DateTimePicker/TimePickerPresenter.cs | 165 +++++++++++++++--- 2 files changed, 172 insertions(+), 40 deletions(-) diff --git a/src/Ursa.Themes.Semi/Controls/TimePicker.axaml b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml index 38dbfe2..b6dd161 100644 --- a/src/Ursa.Themes.Semi/Controls/TimePicker.axaml +++ b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml @@ -4,7 +4,7 @@ xmlns:u="https://irihi.tech/ursa"> - + @@ -12,32 +12,39 @@ - + - - + + + NeedsConfirmationProperty = AvaloniaProperty.Register( nameof(NeedsConfirmation)); @@ -51,17 +77,8 @@ public class TimePickerPresenter: TemplatedControl set => SetValue(TimeProperty, value); } - public static readonly StyledProperty Use12HoursProperty = AvaloniaProperty.Register( - nameof(Use12Hours)); - - public bool Use12Hours - { - get => GetValue(Use12HoursProperty); - set => SetValue(Use12HoursProperty, value); - } - public static readonly StyledProperty PanelFormatProperty = AvaloniaProperty.Register( - nameof(PanelFormat)); + nameof(PanelFormat), defaultValue: "hh mm ss t"); public string PanelFormat { @@ -69,6 +86,72 @@ public class TimePickerPresenter: TemplatedControl set => SetValue(PanelFormatProperty, value); } + static TimePickerPresenter() + { + PanelFormatProperty.Changed.AddClassHandler((presenter, args) => presenter.OnPanelFormatChanged(args)); + } + + private void OnPanelFormatChanged(AvaloniaPropertyChangedEventArgs args) + { + var format = args.NewValue.Value; + + UpdatePanelLayout(format); + } + + private void UpdatePanelLayout(string panelFormat) + { + var parts = panelFormat.Split(' ', '-', ':'); + var panels = new List(); + 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, + }; + IsVisibleProperty.SetValue(true, separator); + } + } + public TimePickerPresenter() { SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay); @@ -82,48 +165,74 @@ public class TimePickerPresenter: TemplatedControl _secondSelector = e.NameScope.Find(PART_SecondSelector); _ampmSelector = e.NameScope.Find(PART_AmPmSelector); _pickerContainer = e.NameScope.Find(PART_PickerContainer); + _hourScrollPanel = e.NameScope.Find(PART_HourScrollPanel); + _minuteScrollPanel = e.NameScope.Find(PART_MinuteScrollPanel); + _secondScrollPanel = e.NameScope.Find(PART_SecondScrollPanel); + _ampmScrollPanel = e.NameScope.Find(PART_AmPmScrollPanel); + _firstSeparator = e.NameScope.Find(PART_FirstSeparator); + _secondSeparator = e.NameScope.Find(PART_SecondSeparator); + _thirdSeparator = e.NameScope.Find(PART_ThirdSeparator); Initialize(); + UpdatePanelLayout(PanelFormat); + UpdatePanelsFromSelectedTime(); + } + + private void UpdatePanelsFromSelectedTime() + { + if (Time is null) return; + var time = Time ?? DateTime.Now.TimeOfDay; + if (_hourSelector is not null) + { + _hourSelector.SelectedValue = _use12Clock ? time.Hours % 12 : time.Hours; + } + if (_minuteSelector is not null) + { + _minuteSelector.SelectedValue = time.Minutes; + } + if (_secondSelector is not null) + { + _secondSelector.SelectedValue = time.Seconds; + } + if (_ampmSelector is not null) + { + _ampmSelector.SelectedValue = time.Hours switch + { + >= 12 => 1, + _ => 0 + }; + } } private void Initialize() { if (_pickerContainer is null) return; - var use12Clock = Use12Hours; if (_hourSelector is not null) { - _hourSelector.MaximumValue = use12Clock ? 12 : 23; - _hourSelector.MinimumValue = use12Clock ? 1 : 0; _hourSelector.ItemFormat = "%h"; - var hour = Time?.Hours; - _hourSelector.SelectedValue = hour ?? 0; + _hourSelector.MaximumValue = _use12Clock ? 12 : 23; + _hourSelector.MinimumValue = _use12Clock ? 1 : 0; + } if(_minuteSelector is not null) { + _minuteSelector.ItemFormat = "mm"; _minuteSelector.MaximumValue = 59; _minuteSelector.MinimumValue = 0; - _minuteSelector.ItemFormat = "mm"; - var minute = Time?.Minutes; - _minuteSelector.SelectedValue = minute ?? 0; + } if(_secondSelector is not null) { + _secondSelector.ItemFormat = "mm"; _secondSelector.MaximumValue = 59; _secondSelector.MinimumValue = 0; - _secondSelector.ItemFormat = "mm"; - var second = Time?.Seconds; - _secondSelector.SelectedValue = second ?? 0; + } if(_ampmSelector is not null) { + _ampmSelector.ItemFormat = "t"; _ampmSelector.MaximumValue = 1; _ampmSelector.MinimumValue = 0; - _ampmSelector.ItemFormat = "%t"; - var ampm = Time?.Hours switch - { - >= 12 => 1, - _ => 0 - }; - _ampmSelector.SelectedValue = ampm; + } } } \ No newline at end of file From 32ad93de60cd3fd5ec3bde5b7136db02f86f96bf Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 26 Apr 2024 18:12:16 +0800 Subject: [PATCH 08/18] feat: disable ampm selector when not in 12 clock mode. --- demo/Ursa.Demo/Pages/TimePickerDemo.axaml | 3 +- .../DateTimePicker/TimePickerPresenter.cs | 76 ++++++++++++++++++- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/demo/Ursa.Demo/Pages/TimePickerDemo.axaml b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml index d5324f6..ecdb17f 100644 --- a/demo/Ursa.Demo/Pages/TimePickerDemo.axaml +++ b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml @@ -6,6 +6,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ursa.Demo.Pages.TimePickerDemo"> - + + diff --git a/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs b/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs index 64f7df0..a447685 100644 --- a/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs +++ b/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs @@ -48,6 +48,8 @@ public class TimePickerPresenter: TemplatedControl private Control? _secondSeparator; private Control? _thirdSeparator; private bool _use12Clock; + private bool _updateFromTimeChange; + internal TimeSpan _timeHolder; public static readonly StyledProperty NeedsConfirmationProperty = AvaloniaProperty.Register( @@ -78,7 +80,7 @@ public class TimePickerPresenter: TemplatedControl } public static readonly StyledProperty PanelFormatProperty = AvaloniaProperty.Register( - nameof(PanelFormat), defaultValue: "hh mm ss t"); + nameof(PanelFormat), defaultValue: "HH mm ss t"); public string PanelFormat { @@ -89,6 +91,14 @@ public class TimePickerPresenter: TemplatedControl static TimePickerPresenter() { PanelFormatProperty.Changed.AddClassHandler((presenter, args) => presenter.OnPanelFormatChanged(args)); + TimeProperty.Changed.AddClassHandler((presenter, args) => presenter.OnTimeChanged(args)); + } + + private void OnTimeChanged(AvaloniaPropertyChangedEventArgs args) + { + _updateFromTimeChange = true; + UpdatePanelsFromSelectedTime(); + _updateFromTimeChange = false; } private void OnPanelFormatChanged(AvaloniaPropertyChangedEventArgs args) @@ -160,10 +170,42 @@ public class TimePickerPresenter: TemplatedControl 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(PART_HourSelector); _minuteSelector = e.NameScope.Find(PART_MinuteSelector); _secondSelector = e.NameScope.Find(PART_SecondSelector); _ampmSelector = e.NameScope.Find(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(PART_PickerContainer); _hourScrollPanel = e.NameScope.Find(PART_HourScrollPanel); _minuteScrollPanel = e.NameScope.Find(PART_MinuteScrollPanel); @@ -177,6 +219,34 @@ public class TimePickerPresenter: TemplatedControl UpdatePanelsFromSelectedTime(); } + private void OnPanelSelectionChanged(object sender, System.EventArgs e) + { + if (_updateFromTimeChange) return; + TimeSpan time = NeedsConfirmation ? _timeHolder : Time ?? DateTime.Now.TimeOfDay; + int hour = _hourSelector?.SelectedValue ?? time.Hours; + int minute = _minuteSelector?.SelectedValue ?? time.Minutes; + int second = _secondSelector?.SelectedValue ?? time.Seconds; + int 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 + }; + } + var newTime = new TimeSpan(hour, minute, second); + if (NeedsConfirmation) + { + _timeHolder = newTime; + } + else + { + SetCurrentValue(TimeProperty, newTime); + } + } + private void UpdatePanelsFromSelectedTime() { if (Time is null) return; @@ -200,6 +270,7 @@ public class TimePickerPresenter: TemplatedControl >= 12 => 1, _ => 0 }; + _ampmSelector.IsEnabled = _use12Clock; } } @@ -208,7 +279,7 @@ public class TimePickerPresenter: TemplatedControl if (_pickerContainer is null) return; if (_hourSelector is not null) { - _hourSelector.ItemFormat = "%h"; + _hourSelector.ItemFormat = "hh"; _hourSelector.MaximumValue = _use12Clock ? 12 : 23; _hourSelector.MinimumValue = _use12Clock ? 1 : 0; @@ -232,7 +303,6 @@ public class TimePickerPresenter: TemplatedControl _ampmSelector.ItemFormat = "t"; _ampmSelector.MaximumValue = 1; _ampmSelector.MinimumValue = 0; - } } } \ No newline at end of file From 1e5da1869cc7afd93e6e454f677d24dd7ad29170 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 26 Apr 2024 20:07:55 +0800 Subject: [PATCH 09/18] feat: implement timepicker functions. --- demo/Ursa.Demo/Pages/TimePickerDemo.axaml | 2 + .../Controls/TimePicker.axaml | 84 +++++++++-- .../Controls/DateTimePicker/TimePicker.cs | 138 ++++++++++++++++-- .../DateTimePicker/TimePickerPresenter.cs | 23 ++- 4 files changed, 221 insertions(+), 26 deletions(-) diff --git a/demo/Ursa.Demo/Pages/TimePickerDemo.axaml b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml index ecdb17f..7c024c6 100644 --- a/demo/Ursa.Demo/Pages/TimePickerDemo.axaml +++ b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml @@ -8,5 +8,7 @@ + + diff --git a/src/Ursa.Themes.Semi/Controls/TimePicker.axaml b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml index b6dd161..f294ab8 100644 --- a/src/Ursa.Themes.Semi/Controls/TimePicker.axaml +++ b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml @@ -1,6 +1,7 @@  @@ -34,12 +35,12 @@ ShouldLoop="True" /> + VerticalAlignment="Stretch" + Fill="{DynamicResource DateTimePickerSeparatorBackground}" /> + VerticalAlignment="Stretch" + Fill="{DynamicResource DateTimePickerSeparatorBackground}" /> + VerticalAlignment="Stretch" + Fill="{DynamicResource DateTimePickerSeparatorBackground}" /> + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa/Controls/DateTimePicker/TimePicker.cs b/src/Ursa/Controls/DateTimePicker/TimePicker.cs index 9cacca4..83632a4 100644 --- a/src/Ursa/Controls/DateTimePicker/TimePicker.cs +++ b/src/Ursa/Controls/DateTimePicker/TimePicker.cs @@ -1,14 +1,32 @@ -using Avalonia; +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; -public class TimePicker : TemplatedControl, IClearControl +[TemplatePart( PART_TextBox, typeof(TextBox))] +[TemplatePart( PART_Popup, typeof(Popup))] +[TemplatePart( PART_Presenter, typeof(TimePickerPresenter))] +public class TimePicker : TemplatedControl, IClearControl, IInnerContentControl, IPopupInnerContent { - public static readonly StyledProperty DisplayFormatProperty = AvaloniaProperty.Register( + public const string PART_TextBox = "PART_TextBox"; + public const string PART_Popup = "PART_Popup"; + public const string PART_Presenter = "PART_Presenter"; + + private TextBox? _textBox; + private Popup? _popup; + private TimePickerPresenter? _presenter; + + private bool _updateFromPresenter; + + public static readonly StyledProperty DisplayFormatProperty = AvaloniaProperty.Register( nameof(DisplayFormat), "HH:mm:ss"); public static readonly StyledProperty PanelFormatProperty = AvaloniaProperty.Register( @@ -21,8 +39,40 @@ public class TimePicker : TemplatedControl, IClearControl public static readonly StyledProperty NeedConfirmationProperty = AvaloniaProperty.Register( nameof(NeedConfirmation)); - public static readonly StyledProperty IsLoopingProperty = AvaloniaProperty.Register( - nameof(IsLooping)); + public static readonly StyledProperty InnerLeftContentProperty = + AvaloniaProperty.Register( + nameof(InnerLeftContent)); + + public static readonly StyledProperty InnerRightContentProperty = + AvaloniaProperty.Register( + nameof(InnerRightContent)); + + + public static readonly StyledProperty PopupInnerTopContentProperty = + AvaloniaProperty.Register( + nameof(PopupInnerTopContent)); + + public static readonly StyledProperty PopupInnerBottomContentProperty = + AvaloniaProperty.Register( + nameof(PopupInnerBottomContent)); + + public static readonly StyledProperty WatermarkProperty = AvaloniaProperty.Register( + nameof(Watermark)); + + public static readonly StyledProperty IsDropdownOpenProperty = AvaloniaProperty.Register( + nameof(IsDropdownOpen), defaultBindingMode: BindingMode.TwoWay); + + public bool IsDropdownOpen + { + get => GetValue(IsDropdownOpenProperty); + set => SetValue(IsDropdownOpenProperty, value); + } + + public string? Watermark + { + get => GetValue(WatermarkProperty); + set => SetValue(WatermarkProperty, value); + } private TimeSpan? _selectedTimeHolder; @@ -30,9 +80,11 @@ public class TimePicker : TemplatedControl, IClearControl { PanelFormatProperty.Changed.AddClassHandler((picker, args) => picker.OnPanelFormatChanged(args)); + SelectedTimeProperty.Changed.AddClassHandler((picker, args) => + picker.OnSelectionChanged(args)); } - public string DisplayFormat + public string? DisplayFormat { get => GetValue(DisplayFormatProperty); set => SetValue(DisplayFormatProperty, value); @@ -56,17 +108,35 @@ public class TimePicker : TemplatedControl, IClearControl set => SetValue(NeedConfirmationProperty, value); } - public bool IsLooping - { - get => GetValue(IsLoopingProperty); - set => SetValue(IsLoopingProperty, value); - } - public void Clear() { SetCurrentValue(SelectedTimeProperty, 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); + } + private void OnPanelFormatChanged(AvaloniaPropertyChangedEventArgs args) { var format = args.NewValue.Value; @@ -77,14 +147,50 @@ public class TimePicker : TemplatedControl, IClearControl protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); + + _textBox = e.NameScope.Find(PART_TextBox); + _popup = e.NameScope.Find(PART_Popup); + _presenter = e.NameScope.Find(PART_Presenter); + TextBox.GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox); + TextBox.TextChangedEvent.AddDisposableHandler(OnTextChanged, _textBox); + TextBox.PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox); } - private void OnSelectionChanged() + private void OnTextBoxPointerPressed(object sender, PointerPressedEventArgs e) { - if (NeedConfirmation) - _selectedTimeHolder = new TimeSpan(); + SetCurrentValue(IsDropdownOpenProperty, true); + } + + private void OnTextBoxGetFocus(object sender, GotFocusEventArgs e) + { + IsDropdownOpen = true; + } + + + private void OnTextChanged(object sender, TextChangedEventArgs e) + { + if (DisplayFormat is null || DisplayFormat.Length == 0) + { + if (TimeSpan.TryParse(_textBox?.Text, out var defaultTime)) + { + TimePickerPresenter.TimeProperty.SetValue(defaultTime, _presenter); + } + } else - SelectedTime = new TimeSpan(); + { + if(DateTime.TryParseExact(_textBox?.Text, DisplayFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None, out var time)) + { + TimePickerPresenter.TimeProperty.SetValue(time.TimeOfDay, _presenter); + } + } + } + + private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs args) + { + if (_textBox is null) return; + var time = args.NewValue.Value; + var text = new DateTime(1,1,1, time?.Hours ?? 0, time?.Minutes ?? 0, time?.Seconds ?? 0).ToString(DisplayFormat); + _textBox.Text = text; } public void Confirm() diff --git a/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs b/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs index a447685..c93930a 100644 --- a/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs +++ b/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs @@ -70,6 +70,15 @@ public class TimePickerPresenter: TemplatedControl set => SetValue(MinuteIncrementProperty, value); } + public static readonly StyledProperty SecondIncrementProperty = AvaloniaProperty.Register( + nameof(SecondIncrement)); + + public int SecondIncrement + { + get => GetValue(SecondIncrementProperty); + set => SetValue(SecondIncrementProperty, value); + } + public static readonly StyledProperty TimeProperty = AvaloniaProperty.Register( nameof(Time)); @@ -88,6 +97,8 @@ public class TimePickerPresenter: TemplatedControl set => SetValue(PanelFormatProperty, value); } + public event EventHandler? SelectedTimeChanged; + static TimePickerPresenter() { PanelFormatProperty.Changed.AddClassHandler((presenter, args) => presenter.OnPanelFormatChanged(args)); @@ -99,6 +110,8 @@ public class TimePickerPresenter: TemplatedControl _updateFromTimeChange = true; UpdatePanelsFromSelectedTime(); _updateFromTimeChange = false; + SelectedTimeChanged?.Invoke(this, + new TimePickerSelectedValueChangedEventArgs(args.OldValue.Value, args.NewValue.Value)); } private void OnPanelFormatChanged(AvaloniaPropertyChangedEventArgs args) @@ -110,7 +123,7 @@ public class TimePickerPresenter: TemplatedControl private void UpdatePanelLayout(string panelFormat) { - var parts = panelFormat.Split(' ', '-', ':'); + var parts = panelFormat.Split(new[] { ' ', '-', ':' }, StringSplitOptions.RemoveEmptyEntries); var panels = new List(); foreach (var part in parts) { @@ -305,4 +318,12 @@ public class TimePickerPresenter: TemplatedControl _ampmSelector.MinimumValue = 0; } } + + public void Confirm() + { + if (NeedsConfirmation) + { + SetCurrentValue(TimeProperty, _timeHolder); + } + } } \ No newline at end of file From 63b9ccfe5e2eac124c4a898b38d7479d1fa1a02b Mon Sep 17 00:00:00 2001 From: rabbitism Date: Fri, 26 Apr 2024 20:34:20 +0800 Subject: [PATCH 10/18] feat: fix several selection issue related to 12 clock mode. --- demo/Ursa.Demo/Pages/TimePickerDemo.axaml | 7 +- .../Controls/TimePicker.axaml | 15 ++-- .../Controls/DateTimePicker/TimePicker.cs | 76 +++++++++---------- .../DateTimePicker/TimePickerPresenter.cs | 4 +- 4 files changed, 52 insertions(+), 50 deletions(-) diff --git a/demo/Ursa.Demo/Pages/TimePickerDemo.axaml b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml index 7c024c6..7083ac4 100644 --- a/demo/Ursa.Demo/Pages/TimePickerDemo.axaml +++ b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml @@ -6,9 +6,8 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ursa.Demo.Pages.TimePickerDemo"> - - - - + + + diff --git a/src/Ursa.Themes.Semi/Controls/TimePicker.axaml b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml index f294ab8..d3f2259 100644 --- a/src/Ursa.Themes.Semi/Controls/TimePicker.axaml +++ b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml @@ -152,11 +152,16 @@ Placement="BottomEdgeAlignedLeft" PlacementTarget="Background"> - + + + + + + diff --git a/src/Ursa/Controls/DateTimePicker/TimePicker.cs b/src/Ursa/Controls/DateTimePicker/TimePicker.cs index 83632a4..9232c87 100644 --- a/src/Ursa/Controls/DateTimePicker/TimePicker.cs +++ b/src/Ursa/Controls/DateTimePicker/TimePicker.cs @@ -11,23 +11,18 @@ 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_TextBox, typeof(TextBox))] +[TemplatePart(PART_Popup, typeof(Popup))] +[TemplatePart(PART_Presenter, typeof(TimePickerPresenter))] 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"; - - private TextBox? _textBox; - private Popup? _popup; - private TimePickerPresenter? _presenter; - private bool _updateFromPresenter; - - public static readonly StyledProperty DisplayFormatProperty = AvaloniaProperty.Register( - nameof(DisplayFormat), "HH:mm:ss"); + public static readonly StyledProperty DisplayFormatProperty = + AvaloniaProperty.Register( + nameof(DisplayFormat), "HH:mm:ss"); public static readonly StyledProperty PanelFormatProperty = AvaloniaProperty.Register( nameof(PanelFormat), "HH mm ss"); @@ -62,6 +57,19 @@ public class TimePicker : TemplatedControl, IClearControl, IInnerContentControl, public static readonly StyledProperty IsDropdownOpenProperty = AvaloniaProperty.Register( nameof(IsDropdownOpen), defaultBindingMode: BindingMode.TwoWay); + private Popup? _popup; + private TimePickerPresenter? _presenter; + + private TextBox? _textBox; + + private bool _updateFromText; + + static TimePicker() + { + SelectedTimeProperty.Changed.AddClassHandler((picker, args) => + picker.OnSelectionChanged(args)); + } + public bool IsDropdownOpen { get => GetValue(IsDropdownOpenProperty); @@ -74,16 +82,6 @@ public class TimePicker : TemplatedControl, IClearControl, IInnerContentControl, set => SetValue(WatermarkProperty, value); } - private TimeSpan? _selectedTimeHolder; - - static TimePicker() - { - PanelFormatProperty.Changed.AddClassHandler((picker, args) => - picker.OnPanelFormatChanged(args)); - SelectedTimeProperty.Changed.AddClassHandler((picker, args) => - picker.OnSelectionChanged(args)); - } - public string? DisplayFormat { get => GetValue(DisplayFormatProperty); @@ -137,23 +135,18 @@ public class TimePicker : TemplatedControl, IClearControl, IInnerContentControl, set => SetValue(PopupInnerBottomContentProperty, value); } - private void OnPanelFormatChanged(AvaloniaPropertyChangedEventArgs args) - { - var format = args.NewValue.Value; - var parts = format.Split(' ', '-', ':'); - } - protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - + _textBox = e.NameScope.Find(PART_TextBox); _popup = e.NameScope.Find(PART_Popup); _presenter = e.NameScope.Find(PART_Presenter); - TextBox.GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox); + GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox); TextBox.TextChangedEvent.AddDisposableHandler(OnTextChanged, _textBox); - TextBox.PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox); + PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox); + SetCurrentValue(SelectedTimeProperty, DateTime.Now.TimeOfDay); } private void OnTextBoxPointerPressed(object sender, PointerPressedEventArgs e) @@ -169,35 +162,38 @@ public class TimePicker : TemplatedControl, IClearControl, IInnerContentControl, private void OnTextChanged(object sender, TextChangedEventArgs e) { + _updateFromText = true; 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); - } + if (DateTime.TryParseExact(_textBox?.Text, DisplayFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None, + out var time)) TimePickerPresenter.TimeProperty.SetValue(time.TimeOfDay, _presenter); } + _updateFromText = false; } private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs args) { if (_textBox is null) return; var time = args.NewValue.Value; - var text = new DateTime(1,1,1, time?.Hours ?? 0, time?.Minutes ?? 0, time?.Seconds ?? 0).ToString(DisplayFormat); + DateTime date = new DateTime(1, 1, 1, time?.Hours ?? 0, time?.Minutes ?? 0, time?.Seconds ?? 0); + var text = date.ToString(DisplayFormat); _textBox.Text = text; } public void Confirm() { - if (NeedConfirmation) - // TODO: close popup. - SetCurrentValue(SelectedTimeProperty, _selectedTimeHolder); + _presenter?.Confirm(); + SetCurrentValue(IsDropdownOpenProperty, false); + } + + public void Dismiss() + { + SetCurrentValue(IsDropdownOpenProperty, false); } protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) diff --git a/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs b/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs index c93930a..5a97bac 100644 --- a/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs +++ b/src/Ursa/Controls/DateTimePicker/TimePickerPresenter.cs @@ -266,7 +266,9 @@ public class TimePickerPresenter: TemplatedControl var time = Time ?? DateTime.Now.TimeOfDay; if (_hourSelector is not null) { - _hourSelector.SelectedValue = _use12Clock ? time.Hours % 12 : time.Hours; + var index = _use12Clock ? time.Hours % 12 : time.Hours; + if (index == 0) index = 12; + _hourSelector.SelectedValue = index; } if (_minuteSelector is not null) { From 9cce1cc1802b9bbf1b2b742e69205af77eaae194 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sat, 27 Apr 2024 00:39:50 +0800 Subject: [PATCH 11/18] feat: fix separator visibility. add dropdown button. --- demo/Ursa.Demo/Pages/TimePickerDemo.axaml | 35 +++-- .../Controls/TimePicker.axaml | 128 ++++++++++++++---- .../Controls/DateTimePicker/TimePicker.cs | 39 ++++-- .../DateTimePicker/TimePickerPresenter.cs | 2 +- 4 files changed, 159 insertions(+), 45 deletions(-) diff --git a/demo/Ursa.Demo/Pages/TimePickerDemo.axaml b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml index 7083ac4..5268b1b 100644 --- a/demo/Ursa.Demo/Pages/TimePickerDemo.axaml +++ b/demo/Ursa.Demo/Pages/TimePickerDemo.axaml @@ -1,13 +1,28 @@ - + - - - + + + + diff --git a/src/Ursa.Themes.Semi/Controls/TimePicker.axaml b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml index d3f2259..ab05067 100644 --- a/src/Ursa.Themes.Semi/Controls/TimePicker.axaml +++ b/src/Ursa.Themes.Semi/Controls/TimePicker.axaml @@ -2,6 +2,7 @@ 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"> @@ -102,6 +103,11 @@ + + + + + @@ -115,34 +121,61 @@ BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" /> - + - - - + Background="Transparent" + BorderBrush="Transparent" + BorderThickness="0" + Foreground="{TemplateBinding Foreground}" + InnerLeftContent="{TemplateBinding InnerLeftContent}" + InnerRightContent="{TemplateBinding InnerRightContent}" + IsReadOnly="{TemplateBinding IsReadonly}" + Theme="{DynamicResource NoErrorTextBox}" + Watermark="{TemplateBinding Watermark}"> + + + + + + + + +