diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 5a376b8c39..ca210300ee 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -29,6 +29,9 @@ ScrollViewer.HorizontalScrollBarVisibility="Disabled"> + + + @@ -54,6 +57,7 @@ + diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml new file mode 100644 index 0000000000..af6b6e8605 --- /dev/null +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml @@ -0,0 +1,123 @@ + + + DatePicker and TimePicker + + + + + A simple DatePicker with a header + + + + + + + + + <DatePicker Header="Pick a date" /> + + + + + + + A DatePicker with day formatted and year hidden. + + + + + + + + + <DatePicker DayFormat="d (ddd)" YearVisible="False" /> + + + + + + + + + A simple TimePicker. + + + + + + + + + <TimePicker /> + + + + + + + A TimePicker with a header and minute increments specified. + + + + + + + + + <TimePicker Header="Arrival time" MinuteIncrement="15" /> + + + + + + + A TimePicker using a 12-hour clock. + + + + + + + + + <TimePicker ClockIdentifier="12HourClock" Header="12 hour clock" /> + + + + + + + A TimePicker using a 24-hour clock. + + + + + + + + + <TimePicker ClockIdentifier="24HourClock" Header="24 hour clock" /> + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs new file mode 100644 index 0000000000..6c7ae3437e --- /dev/null +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs @@ -0,0 +1,30 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class DateTimePickerPage : UserControl + { + public DateTimePickerPage() + { + this.InitializeComponent(); + this.FindControl("DatePickerDesc").Text = "Use a DatePicker to let users set a date in your app, " + + "for example to schedule an appointment. The DatePicker displays three controls for month, day, and year. " + + "These controls are easy to use with touch or mouse, and they can be styled and configured in several different ways. " + + "Order of month, day, and year is dynamically set based on user date settings"; + + this.FindControl("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " + + "to set a reminder. The TimePicker displays three controls for hour, minute, and AM / PM(if necessary).These controls " + + "are easy to use with touch or mouse, and they can be styled and configured in several different ways. " + + "12 - hour or 24 - hour clock and visiblility of AM / PM is dynamically set based on user time settings, or can be overridden."; + + + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml b/samples/ControlCatalog/Pages/SplitViewPage.xaml new file mode 100644 index 0000000000..7e629db2da --- /dev/null +++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml @@ -0,0 +1,97 @@ + + + + + + + + + + + + + + + + Inline + CompactInline + Overlay + CompactOverlay + + + + + SystemControlBackgroundChromeMediumLowBrush + Red + Blue + Green + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs b/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs new file mode 100644 index 0000000000..cbf217c94a --- /dev/null +++ b/samples/ControlCatalog/Pages/SplitViewPage.xaml.cs @@ -0,0 +1,21 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; + +namespace ControlCatalog.Pages +{ + public class SplitViewPage : UserControl + { + public SplitViewPage() + { + this.InitializeComponent(); + DataContext = new SplitViewPageViewModel(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs b/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs new file mode 100644 index 0000000000..f27f605a8b --- /dev/null +++ b/samples/ControlCatalog/ViewModels/SplitViewPageViewModel.cs @@ -0,0 +1,46 @@ +using System; +using Avalonia.Controls; +using ReactiveUI; + +namespace ControlCatalog.ViewModels +{ + public class SplitViewPageViewModel : ReactiveObject + { + private bool _isLeft = true; + private int _displayMode = 3; //CompactOverlay + + public bool IsLeft + { + get => _isLeft; + set + { + this.RaiseAndSetIfChanged(ref _isLeft, value); + this.RaisePropertyChanged(nameof(PanePlacement)); + } + } + + public int DisplayMode + { + get => _displayMode; + set + { + this.RaiseAndSetIfChanged(ref _displayMode, value); + this.RaisePropertyChanged(nameof(CurrentDisplayMode)); + } + } + + public SplitViewPanePlacement PanePlacement => _isLeft ? SplitViewPanePlacement.Left : SplitViewPanePlacement.Right; + + public SplitViewDisplayMode CurrentDisplayMode + { + get + { + if (Enum.IsDefined(typeof(SplitViewDisplayMode), _displayMode)) + { + return (SplitViewDisplayMode)_displayMode; + } + return SplitViewDisplayMode.CompactOverlay; + } + } + } +} diff --git a/src/Avalonia.Animation/Easing/Easing.cs b/src/Avalonia.Animation/Easing/Easing.cs index 5b0dea6c60..e006459652 100644 --- a/src/Avalonia.Animation/Easing/Easing.cs +++ b/src/Avalonia.Animation/Easing/Easing.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Globalization; using System.Linq; namespace Avalonia.Animation.Easings @@ -25,6 +26,11 @@ namespace Avalonia.Animation.Easings /// Returns the instance of the parsed type. public static Easing Parse(string e) { + if (e.Contains(',')) + { + return new SplineEasing(KeySpline.Parse(e, CultureInfo.InvariantCulture)); + } + if (_easingTypes == null) { _easingTypes = new Dictionary(); diff --git a/src/Avalonia.Animation/Easing/SplineEasing.cs b/src/Avalonia.Animation/Easing/SplineEasing.cs new file mode 100644 index 0000000000..975fcc4746 --- /dev/null +++ b/src/Avalonia.Animation/Easing/SplineEasing.cs @@ -0,0 +1,85 @@ +namespace Avalonia.Animation.Easings +{ + /// + /// Eases a value + /// using a user-defined cubic bezier curve. + /// Good for custom easing functions that doesn't quite + /// fit with the built-in ones. + /// + public class SplineEasing : Easing + { + /// + /// X coordinate of the first control point + /// + public double X1 + { + get => _internalKeySpline.ControlPointX1; + set + { + _internalKeySpline.ControlPointX1 = value; + } + } + + /// + /// Y coordinate of the first control point + /// + public double Y1 + { + get => _internalKeySpline.ControlPointY1; + set + { + _internalKeySpline.ControlPointY1 = value; + } + } + + /// + /// X coordinate of the second control point + /// + public double X2 + { + get => _internalKeySpline.ControlPointX2; + set + { + _internalKeySpline.ControlPointX2 = value; + } + } + + /// + /// Y coordinate of the second control point + /// + public double Y2 + { + get => _internalKeySpline.ControlPointY2; + set + { + _internalKeySpline.ControlPointY2 = value; + } + } + + private readonly KeySpline _internalKeySpline; + + public SplineEasing(double x1 = 0d, double y1 = 0d, double x2 = 1d, double y2 = 1d) + { + _internalKeySpline = new KeySpline(); + + this.X1 = x1; + this.Y1 = y1; + this.X2 = x2; + this.Y1 = y2; + } + + public SplineEasing(KeySpline keySpline) + { + _internalKeySpline = keySpline; + } + + public SplineEasing() + { + _internalKeySpline = new KeySpline(); + } + + /// + public override double Ease(double progress) => + _internalKeySpline.GetSplineProgress(progress); + } +} diff --git a/src/Avalonia.Animation/KeySpline.cs b/src/Avalonia.Animation/KeySpline.cs index 5a4f7a15a3..a6e9769186 100644 --- a/src/Avalonia.Animation/KeySpline.cs +++ b/src/Avalonia.Animation/KeySpline.cs @@ -81,7 +81,10 @@ namespace Avalonia.Animation /// A with the appropriate values set public static KeySpline Parse(string value, CultureInfo culture) { - using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline.")) + if (culture is null) + culture = CultureInfo.InvariantCulture; + + using (var tokenizer = new StringTokenizer((string)value, culture, exceptionMessage: $"Invalid KeySpline string: \"{value}\".")) { return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble()); } @@ -98,6 +101,7 @@ namespace Avalonia.Animation if (IsValidXValue(value)) { _controlPointX1 = value; + _isDirty = true; } else { @@ -112,7 +116,11 @@ namespace Avalonia.Animation public double ControlPointY1 { get => _controlPointY1; - set => _controlPointY1 = value; + set + { + _controlPointY1 = value; + _isDirty = true; + } } /// @@ -126,6 +134,7 @@ namespace Avalonia.Animation if (IsValidXValue(value)) { _controlPointX2 = value; + _isDirty = true; } else { @@ -140,7 +149,11 @@ namespace Avalonia.Animation public double ControlPointY2 { get => _controlPointY2; - set => _controlPointY2 = value; + set + { + _controlPointY2 = value; + _isDirty = true; + } } /// @@ -330,20 +343,4 @@ namespace Avalonia.Animation } } } - - /// - /// Converts string values to values - /// - public class KeySplineTypeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - return KeySpline.Parse((string)value, culture); - } - } } diff --git a/src/Avalonia.Animation/KeySplineTypeConverter.cs b/src/Avalonia.Animation/KeySplineTypeConverter.cs new file mode 100644 index 0000000000..cd7427a37d --- /dev/null +++ b/src/Avalonia.Animation/KeySplineTypeConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.ComponentModel; +using System.Globalization; + +// Ported from WPF open-source code. +// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs + +namespace Avalonia.Animation +{ + /// + /// Converts string values to values + /// + public class KeySplineTypeConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + return KeySpline.Parse((string)value, culture); + } + } +} diff --git a/src/Avalonia.Controls/DateTimePickers/DatePicker.cs b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs new file mode 100644 index 0000000000..5d3311e8c6 --- /dev/null +++ b/src/Avalonia.Controls/DateTimePickers/DatePicker.cs @@ -0,0 +1,412 @@ +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Controls.Templates; +using Avalonia.Interactivity; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace Avalonia.Controls +{ + /// + /// A control to allow the user to select a date + /// + public class DatePicker : TemplatedControl + { + /// + /// Define the Property + /// + public static readonly DirectProperty DayFormatProperty = + AvaloniaProperty.RegisterDirect(nameof(DayFormat), + x => x.DayFormat, (x, v) => x.DayFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty DayVisibleProperty = + AvaloniaProperty.RegisterDirect(nameof(DayVisible), + x => x.DayVisible, (x, v) => x.DayVisible = v); + + /// + /// Defines the Property + /// + public static readonly StyledProperty HeaderProperty = + AvaloniaProperty.Register(nameof(Header)); + + /// + /// Defines the Property + /// + public static readonly StyledProperty HeaderTemplateProperty = + AvaloniaProperty.Register(nameof(HeaderTemplate)); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MaxYearProperty = + AvaloniaProperty.RegisterDirect(nameof(MaxYear), + x => x.MaxYear, (x, v) => x.MaxYear = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MinYearProperty = + AvaloniaProperty.RegisterDirect(nameof(MinYear), + x => x.MinYear, (x, v) => x.MinYear = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MonthFormatProperty = + AvaloniaProperty.RegisterDirect(nameof(MonthFormat), + x => x.MonthFormat, (x, v) => x.MonthFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty MonthVisibleProperty = + AvaloniaProperty.RegisterDirect(nameof(MonthVisible), + x => x.MonthVisible, (x, v) => x.MonthVisible = v); + + /// + /// Defiens the Property + /// + public static readonly DirectProperty YearFormatProperty = + AvaloniaProperty.RegisterDirect(nameof(YearFormat), + x => x.YearFormat, (x, v) => x.YearFormat = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty YearVisibleProperty = + AvaloniaProperty.RegisterDirect(nameof(YearVisible), + x => x.YearVisible, (x, v) => x.YearVisible = v); + + /// + /// Defines the Property + /// + public static readonly DirectProperty SelectedDateProperty = + AvaloniaProperty.RegisterDirect(nameof(SelectedDate), + x => x.SelectedDate, (x, v) => x.SelectedDate = v); + + //Template Items + private Button _flyoutButton; + private TextBlock _dayText; + private TextBlock _monthText; + private TextBlock _yearText; + private Grid _container; + private Rectangle _spacer1; + private Rectangle _spacer2; + private Popup _popup; + private DatePickerPresenter _presenter; + + private bool _areControlsAvailable; + + private string _dayFormat = "%d"; + private bool _dayVisible = true; + private DateTimeOffset _maxYear; + private DateTimeOffset _minYear; + private string _monthFormat = "MMMM"; + private bool _monthVisible = true; + private string _yearFormat = "yyyy"; + private bool _yearVisible = true; + private DateTimeOffset? _selectedDate; + + public DatePicker() + { + PseudoClasses.Set(":hasnodate", true); + var now = DateTimeOffset.Now; + _minYear = new DateTimeOffset(now.Date.Year - 100, 1, 1, 0, 0, 0, now.Offset); + _maxYear = new DateTimeOffset(now.Date.Year + 100, 12, 31, 0, 0, 0, now.Offset); + } + + public string DayFormat + { + get => _dayFormat; + set => SetAndRaise(DayFormatProperty, ref _dayFormat, value); + } + + /// + /// Gets or sets whether the day is visible + /// + public bool DayVisible + { + get => _dayVisible; + set + { + SetAndRaise(DayVisibleProperty, ref _dayVisible, value); + SetGrid(); + } + } + + /// + /// Gets or sets the DatePicker header + /// + public object Header + { + get => GetValue(HeaderProperty); + set => SetValue(HeaderProperty, value); + } + + /// + /// Gets or sets the header template + /// + public IDataTemplate HeaderTemplate + { + get => GetValue(HeaderTemplateProperty); + set => SetValue(HeaderTemplateProperty, value); + } + + /// + /// Gets or sets the maximum year for the picker + /// + public DateTimeOffset MaxYear + { + get => _maxYear; + set + { + if (value < MinYear) + throw new InvalidOperationException("MaxDate cannot be less than MinDate"); + SetAndRaise(MaxYearProperty, ref _maxYear, value); + + if (SelectedDate.HasValue && SelectedDate.Value > value) + SelectedDate = value; + } + } + + /// + /// Gets or sets the minimum year for the picker + /// + public DateTimeOffset MinYear + { + get => _minYear; + set + { + if (value > MaxYear) + throw new InvalidOperationException("MinDate cannot be greater than MaxDate"); + SetAndRaise(MinYearProperty, ref _minYear, value); + + if (SelectedDate.HasValue && SelectedDate.Value < value) + SelectedDate = value; + } + } + + /// + /// Gets or sets the month format + /// + public string MonthFormat + { + get => _monthFormat; + set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value); + } + + /// + /// Gets or sets whether the month is visible + /// + public bool MonthVisible + { + get => _monthVisible; + set + { + SetAndRaise(MonthVisibleProperty, ref _monthVisible, value); + SetGrid(); + } + } + + /// + /// Gets or sets the year format + /// + public string YearFormat + { + get => _yearFormat; + set => SetAndRaise(YearFormatProperty, ref _yearFormat, value); + } + + /// + /// Gets or sets whether the year is visible + /// + public bool YearVisible + { + get => _yearVisible; + set + { + SetAndRaise(YearVisibleProperty, ref _yearVisible, value); + SetGrid(); + } + } + + /// + /// Gets or sets the Selected Date for the picker, can be null + /// + public DateTimeOffset? SelectedDate + { + get => _selectedDate; + set + { + var old = _selectedDate; + SetAndRaise(SelectedDateProperty, ref _selectedDate, value); + SetSelectedDateText(); + OnSelectedDateChanged(this, new DatePickerSelectedValueChangedEventArgs(old, value)); + } + } + + /// + /// Raised when the changes + /// + public event EventHandler SelectedDateChanged; + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + _areControlsAvailable = false; + if (_flyoutButton != null) + _flyoutButton.Click -= OnFlyoutButtonClicked; + if (_presenter != null) + { + _presenter.Confirmed -= OnConfirmed; + _presenter.Dismissed -= OnDismissPicker; + } + + base.OnApplyTemplate(e); + _flyoutButton = e.NameScope.Find