From d91b8f8f84a870949a8247e93db610c1f7be696d Mon Sep 17 00:00:00 2001 From: sdoroff Date: Mon, 23 Oct 2017 13:31:01 -0400 Subject: [PATCH 1/7] Adds a Calendar control ported from Silverlight --- samples/ControlCatalog/ControlCatalog.csproj | 6 + samples/ControlCatalog/MainView.xaml | 1 + .../ControlCatalog/Pages/CalendarPage.xaml | 47 + .../ControlCatalog/Pages/CalendarPage.xaml.cs | 28 + src/Avalonia.Controls/Calendar/Calendar.cs | 2129 +++++++++++++++++ .../CalendarBlackoutDatesCollection.cs | 223 ++ .../Calendar/CalendarButton.cs | 193 ++ .../Calendar/CalendarDateRange.cs | 79 + .../Calendar/CalendarDayButton.cs | 253 ++ .../Calendar/CalendarExtensions.cs | 78 + .../Calendar/CalendarItem.cs | 1263 ++++++++++ .../Calendar/DateTimeHelper.cs | 155 ++ .../Calendar/SelectedDatesCollection.cs | 377 +++ src/Avalonia.Themes.Default/Calendar.xaml | 30 + .../CalendarButton.xaml | 80 + .../CalendarDayButton.xaml | 116 + src/Avalonia.Themes.Default/CalendarItem.xaml | 209 ++ src/Avalonia.Themes.Default/DefaultTheme.xaml | 4 + 18 files changed, 5271 insertions(+) create mode 100644 samples/ControlCatalog/Pages/CalendarPage.xaml create mode 100644 samples/ControlCatalog/Pages/CalendarPage.xaml.cs create mode 100644 src/Avalonia.Controls/Calendar/Calendar.cs create mode 100644 src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs create mode 100644 src/Avalonia.Controls/Calendar/CalendarButton.cs create mode 100644 src/Avalonia.Controls/Calendar/CalendarDateRange.cs create mode 100644 src/Avalonia.Controls/Calendar/CalendarDayButton.cs create mode 100644 src/Avalonia.Controls/Calendar/CalendarExtensions.cs create mode 100644 src/Avalonia.Controls/Calendar/CalendarItem.cs create mode 100644 src/Avalonia.Controls/Calendar/DateTimeHelper.cs create mode 100644 src/Avalonia.Controls/Calendar/SelectedDatesCollection.cs create mode 100644 src/Avalonia.Themes.Default/Calendar.xaml create mode 100644 src/Avalonia.Themes.Default/CalendarButton.xaml create mode 100644 src/Avalonia.Themes.Default/CalendarDayButton.xaml create mode 100644 src/Avalonia.Themes.Default/CalendarItem.xaml diff --git a/samples/ControlCatalog/ControlCatalog.csproj b/samples/ControlCatalog/ControlCatalog.csproj index 11ff531514..0b4463ddb7 100644 --- a/samples/ControlCatalog/ControlCatalog.csproj +++ b/samples/ControlCatalog/ControlCatalog.csproj @@ -44,6 +44,9 @@ Designer + + Designer + Designer @@ -107,6 +110,9 @@ ButtonPage.xaml + + CalendarPage.xaml + CanvasPage.xaml diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 0940316ce9..311a61a6dc 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -7,6 +7,7 @@ + diff --git a/samples/ControlCatalog/Pages/CalendarPage.xaml b/samples/ControlCatalog/Pages/CalendarPage.xaml new file mode 100644 index 0000000000..a433fd1add --- /dev/null +++ b/samples/ControlCatalog/Pages/CalendarPage.xaml @@ -0,0 +1,47 @@ + + + Calendar + A calendar control for selecting dates + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/ControlCatalog/Pages/CalendarPage.xaml.cs b/samples/ControlCatalog/Pages/CalendarPage.xaml.cs new file mode 100644 index 0000000000..e3e9a3444e --- /dev/null +++ b/samples/ControlCatalog/Pages/CalendarPage.xaml.cs @@ -0,0 +1,28 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using System; + +namespace ControlCatalog.Pages +{ + public class CalendarPage : UserControl + { + public CalendarPage() + { + this.InitializeComponent(); + + var today = DateTime.Today; + var cal1 = this.FindControl("DisplayDatesCalendar"); + cal1.DisplayDateStart = today.AddDays(-25); + cal1.DisplayDateEnd = today.AddDays(25); + + var cal2 = this.FindControl("BlackoutDatesCalendar"); + cal2.BlackoutDates.AddDatesInPast(); + cal2.BlackoutDates.Add(new CalendarDateRange(today.AddDays(6))); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs new file mode 100644 index 0000000000..236ac48630 --- /dev/null +++ b/src/Avalonia.Controls/Calendar/Calendar.cs @@ -0,0 +1,2129 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using System; +using System.Collections.ObjectModel; +using System.Diagnostics; + +namespace Avalonia.Controls +{ + /// + /// Specifies values for the different modes of operation of a + /// . + /// + public enum CalendarMode + { + /// + /// The displays a + /// month at a time. + /// + Month = 0, + + /// + /// The displays a + /// year at a time. + /// + Year = 1, + + /// + /// The displays a + /// decade at a time. + /// + Decade = 2, + } + + /// + /// Specifies values that describe the available selection modes for a + /// . + /// + /// + /// This enumeration provides the values that are used by the SelectionMode + /// property. + /// + public enum CalendarSelectionMode + { + /// + /// Only a single date can be selected. Use the + /// + /// property to retrieve the selected date. + /// + SingleDate = 0, + + /// + /// A single range of dates can be selected. Use + /// + /// property to retrieve the selected dates. + /// + SingleRange = 1, + + /// + /// Multiple non-contiguous ranges of dates can be selected. Use the + /// + /// property to retrieve the selected dates. + /// + MultipleRange = 2, + + /// + /// No selections are allowed. + /// + None = 3, + } + + /// + /// Provides data for the + /// + /// event. + /// + public class CalendarDateChangedEventArgs : RoutedEventArgs + { + /// + /// Gets the date that was previously displayed. + /// + /// + /// The date previously displayed. + /// + public DateTime? RemovedDate { get; private set; } + + /// + /// Gets the date to be newly displayed. + /// + /// The new date to display. + public DateTime? AddedDate { get; private set; } + + /// + /// Initializes a new instance of the CalendarDateChangedEventArgs + /// class. + /// + /// + /// The date that was previously displayed. + /// + /// The date to be newly displayed. + internal CalendarDateChangedEventArgs(DateTime? removedDate, DateTime? addedDate) + { + RemovedDate = removedDate; + AddedDate = addedDate; + } + } + + /// + /// Provides data for the + /// + /// event. + /// + /// Mature + public class CalendarModeChangedEventArgs : RoutedEventArgs + { + /// + /// Gets the previous mode of the + /// . + /// + /// + /// A representing + /// the previous mode. + /// + public CalendarMode OldMode { get; private set; } + + /// + /// Gets the new mode of the + /// . + /// + /// + /// A + /// the new mode. + /// + public CalendarMode NewMode { get; private set; } + + /// + /// Initializes a new instance of the + /// + /// class. + /// + /// The previous mode. + /// The new mode. + public CalendarModeChangedEventArgs(CalendarMode oldMode, CalendarMode newMode) + { + OldMode = oldMode; + NewMode = newMode; + } + } + + /// + /// Represents a control that enables a user to select a date by using a + /// visual calendar display. + /// + /// + /// + /// A Calendar control can be used on its own, or as a drop-down part of a + /// DatePicker control. For more information, see DatePicker. A Calendar + /// displays either the days of a month, the months of a year, or the years + /// of a decade, depending on the value of the DisplayMode property. When + /// displaying the days of a month, the user can select a date, a range of + /// dates, or multiple ranges of dates. The kinds of selections that are + /// allowed are controlled by the SelectionMode property. + /// + /// + /// The range of dates displayed is governed by the DisplayDateStart and + /// DisplayDateEnd properties. If DisplayMode is Year or Decade, only + /// months or years that contain displayable dates will be displayed. + /// Setting the displayable range to a range that does not include the + /// current DisplayDate will throw an ArgumentOutOfRangeException. + /// + /// + /// The BlackoutDates property can be used to specify dates that cannot be + /// selected. These dates will be displayed as dimmed and disabled. + /// + /// + /// By default, Today is highlighted. This can be disabled by setting + /// IsTodayHighlighted to false. + /// + /// + /// The Calendar control provides basic navigation using either the mouse or + /// keyboard. The following table summarizes keyboard navigation. + /// + /// Key Combination DisplayMode Action + /// ARROW Any Change focused date, unselect + /// all selected dates, and select + /// new focused date. + /// + /// SHIFT+ARROW Any If SelectionMode is not set to + /// SingleDate or None begin + /// selecting a range of dates. + /// + /// CTRL+UP ARROW Any Switch to the next larger + /// DisplayMode. If DisplayMode is + /// already Decade, no action. + /// + /// CTRL+DOWN ARROW Any Switch to the next smaller + /// DisplayMode. If DisplayMode is + /// already Month, no action. + /// + /// SPACEBAR Month Select focused date. + /// + /// SPACEBAR Year or Decade Switch DisplayMode to the Month + /// or Year represented by focused + /// item. + /// + /// + /// XAML Usage for Classes Derived from Calendar + /// If you define a class that derives from Calendar, the class can be used + /// as an object element in XAML, and all of the inherited properties and + /// events that show a XAML usage in the reference for the Calendar members + /// can have the same XAML usage for the derived class. However, the object + /// element itself must have a different prefix mapping than the controls: + /// mapping shown in the usages, because the derived class comes from an + /// assembly and namespace that you create and define. You must define your + /// own prefix mapping to an XML namespace to use the class as an object + /// element in XAML. + /// + /// + public class Calendar : TemplatedControl + { + internal const int RowsPerMonth = 7; + internal const int ColumnsPerMonth = 7; + internal const int RowsPerYear = 3; + internal const int ColumnsPerYear = 4; + + private DateTime? _selectedDate; + private DateTime _selectedMonth; + private DateTime _selectedYear; + + private DateTime _displayDate = DateTime.Today; + private DateTime? _displayDateStart = null; + private DateTime? _displayDateEnd = null; + + private bool _isShiftPressed; + + internal CalendarDayButton FocusButton { get; set; } + internal CalendarButton FocusCalendarButton { get; set; } + + internal Panel Root { get; set; } + internal CalendarItem MonthControl + { + get + { + + if (Root != null && Root.Children.Count > 0) + { + return Root.Children[0] as CalendarItem; + } + return null; + } + } + + public static readonly StyledProperty FirstDayOfWeekProperty = + AvaloniaProperty.Register( + nameof(FirstDayOfWeek), + defaultValue: DateTimeHelper.GetCurrentDateFormat().FirstDayOfWeek); + /// + /// Gets or sets the day that is considered the beginning of the week. + /// + /// + /// A representing the beginning of + /// the week. The default is . + /// + public DayOfWeek FirstDayOfWeek + { + get { return GetValue(FirstDayOfWeekProperty); } + set { SetValue(FirstDayOfWeekProperty, value); } + } + /// + /// FirstDayOfWeekProperty property changed handler. + /// + /// The DependencyPropertyChangedEventArgs. + private void OnFirstDayOfWeekChanged(AvaloniaPropertyChangedEventArgs e) + { + + if (IsValidFirstDayOfWeek(e.NewValue)) + { + UpdateMonths(); + } + else + { + throw new ArgumentOutOfRangeException("d", "Invalid DayOfWeek"); + } + } + /// + /// Inherited code: Requires comment. + /// + /// Inherited code: Requires comment 1. + /// Inherited code: Requires comment 2. + private static bool IsValidFirstDayOfWeek(object value) + { + DayOfWeek day = (DayOfWeek)value; + + return day == DayOfWeek.Sunday + || day == DayOfWeek.Monday + || day == DayOfWeek.Tuesday + || day == DayOfWeek.Wednesday + || day == DayOfWeek.Thursday + || day == DayOfWeek.Friday + || day == DayOfWeek.Saturday; + } + + public static readonly StyledProperty IsTodayHighlightedProperty = + AvaloniaProperty.Register( + nameof(IsTodayHighlighted), + defaultValue: true); + /// + /// Gets or sets a value indicating whether the current date is + /// highlighted. + /// + /// + /// True if the current date is highlighted; otherwise, false. The + /// default is true. + /// + public bool IsTodayHighlighted + { + get { return GetValue(IsTodayHighlightedProperty); } + set { SetValue(IsTodayHighlightedProperty, value); } + } + /// + /// IsTodayHighlightedProperty property changed handler. + /// + /// The DependencyPropertyChangedEventArgs. + private void OnIsTodayHighlightedChanged(AvaloniaPropertyChangedEventArgs e) + { + if (DisplayDate != null) + { + int i = DateTimeHelper.CompareYearMonth(DisplayDateInternal, DateTime.Today); + + if (i > -2 && i < 2) + { + UpdateMonths(); + } + } + } + + public static readonly StyledProperty HeaderBackgroundProperty = + AvaloniaProperty.Register(nameof(HeaderBackground)); + public IBrush HeaderBackground + { + get { return GetValue(HeaderBackgroundProperty); } + set { SetValue(HeaderBackgroundProperty, value); } + } + + public static readonly StyledProperty DisplayModeProperty = + AvaloniaProperty.Register(nameof(DisplayMode)); + /// + /// Gets or sets a value indicating whether the calendar is displayed in + /// months, years, or decades. + /// + /// + /// A value indicating what length of time the + /// should display. + /// + public CalendarMode DisplayMode + { + get { return GetValue(DisplayModeProperty); } + set { SetValue(DisplayModeProperty, value); } + } + /// + /// DisplayModeProperty property changed handler. + /// + /// The DependencyPropertyChangedEventArgs. + private void OnDisplayModePropertyChanged(AvaloniaPropertyChangedEventArgs e) + { + CalendarMode mode = (CalendarMode)e.NewValue; + CalendarMode oldMode = (CalendarMode)e.OldValue; + CalendarItem monthControl = MonthControl; + + if (!this.IsHandlerSuspended(Calendar.DisplayModeProperty)) + { + if (IsValidDisplayMode(mode)) + { + if (monthControl != null) + { + switch (oldMode) + { + case CalendarMode.Month: + { + SelectedYear = DisplayDateInternal; + SelectedMonth = DisplayDateInternal; + break; + } + case CalendarMode.Year: + { + DisplayDate = SelectedMonth; + SelectedYear = SelectedMonth; + break; + } + case CalendarMode.Decade: + { + DisplayDate = SelectedYear; + SelectedMonth = SelectedYear; + break; + } + } + + switch (mode) + { + case CalendarMode.Month: + { + OnMonthClick(); + break; + } + case CalendarMode.Year: + case CalendarMode.Decade: + { + OnHeaderClick(); + break; + } + } + } + OnDisplayModeChanged(new CalendarModeChangedEventArgs((CalendarMode)e.OldValue, mode)); + } + else + { + this.SetValueNoCallback(Calendar.DisplayModeProperty, (CalendarMode)e.OldValue); + throw new ArgumentOutOfRangeException("d", "Invalid DisplayMode"); + } + } + } + /// + /// Inherited code: Requires comment. + /// + /// Inherited code: Requires comment 1. + /// Inherited code: Requires comment 2. + private static bool IsValidDisplayMode(CalendarMode mode) + { + return mode == CalendarMode.Month + || mode == CalendarMode.Year + || mode == CalendarMode.Decade; + } + private void OnDisplayModeChanged(CalendarModeChangedEventArgs args) + { + DisplayModeChanged?.Invoke(this, args); + } + + public static readonly StyledProperty SelectionModeProperty = + AvaloniaProperty.Register( + nameof(SelectionMode), + defaultValue: CalendarSelectionMode.SingleDate); + /// + /// Gets or sets a value that indicates what kind of selections are + /// allowed. + /// + /// + /// A value that indicates the current selection mode. The default is + /// . + /// + /// + /// + /// This property determines whether the Calendar allows no selection, + /// selection of a single date, or selection of multiple dates. The + /// selection mode is specified with the CalendarSelectionMode + /// enumeration. + /// + /// + /// When this property is changed, all selected dates will be cleared. + /// + /// + public CalendarSelectionMode SelectionMode + { + get { return GetValue(SelectionModeProperty); } + set { SetValue(SelectionModeProperty, value); } + } + private void OnSelectionModeChanged(AvaloniaPropertyChangedEventArgs e) + { + if (IsValidSelectionMode(e.NewValue)) + { + this.SetValueNoCallback(Calendar.SelectedDateProperty, null); + SelectedDates.Clear(); + } + else + { + throw new ArgumentOutOfRangeException("d", "Invalid SelectionMode"); + } + } + /// + /// Inherited code: Requires comment. + /// + /// Inherited code: Requires comment 1. + /// Inherited code: Requires comment 2. + private static bool IsValidSelectionMode(object value) + { + CalendarSelectionMode mode = (CalendarSelectionMode)value; + + return mode == CalendarSelectionMode.SingleDate + || mode == CalendarSelectionMode.SingleRange + || mode == CalendarSelectionMode.MultipleRange + || mode == CalendarSelectionMode.None; + } + + public static readonly DirectProperty SelectedDateProperty = + AvaloniaProperty.RegisterDirect( + nameof(SelectedDate), + o => o.SelectedDate, + (o, v) => o.SelectedDate = v, + defaultBindingMode: BindingMode.TwoWay); + /// + /// Gets or sets the currently selected date. + /// + /// The date currently selected. The default is null. + /// + /// The given date is outside the range specified by + /// + /// and + /// -or- + /// The given date is in the + /// + /// collection. + /// + /// + /// If set to anything other than null when + /// is + /// set to + /// . + /// + /// + /// Use this property when SelectionMode is set to SingleDate. In other + /// modes, this property will always be the first date in SelectedDates. + /// + public DateTime? SelectedDate + { + get { return _selectedDate; } + set { SetAndRaise(SelectedDateProperty, ref _selectedDate, value); } + } + private void OnSelectedDateChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!this.IsHandlerSuspended(Calendar.SelectedDateProperty)) + { + if (SelectionMode != CalendarSelectionMode.None) + { + DateTime? addedDate; + + addedDate = (DateTime?)e.NewValue; + + if (IsValidDateSelection(this, addedDate)) + { + if (addedDate == null) + { + SelectedDates.Clear(); + } + else + { + if (addedDate.HasValue && !(SelectedDates.Count > 0 && SelectedDates[0] == addedDate.Value)) + { + foreach (DateTime item in SelectedDates) + { + RemovedItems.Add(item); + } + SelectedDates.ClearInternal(); + // the value is added as a range so that the + // SelectedDatesChanged event can be thrown with + // all the removed items + SelectedDates.AddRange(addedDate.Value, addedDate.Value); + } + } + + // We update the LastSelectedDate for only the Single + // mode. For the other modes it automatically gets + // updated when the HoverEnd is updated. + if (SelectionMode == CalendarSelectionMode.SingleDate) + { + LastSelectedDate = addedDate; + } + } + else + { + throw new ArgumentOutOfRangeException("d", "SelectedDate value is not valid."); + } + } + else + { + throw new InvalidOperationException("The SelectedDate property cannot be set when the selection mode is None."); + } + } + } + + /// + /// Gets a collection of selected dates. + /// + /// + /// A + /// object that contains the currently selected dates. The default is an + /// empty collection. + /// + /// + /// Dates can be added to the collection either individually or in a + /// range using the AddRange method. Depending on the value of the + /// SelectionMode property, adding a date or range to the collection may + /// cause it to be cleared. The following table lists how + /// CalendarSelectionMode affects the SelectedDates property. + /// + /// CalendarSelectionMode Description + /// None No selections are allowed. SelectedDate + /// cannot be set and no values can be added + /// to SelectedDates. + /// + /// SingleDate Only a single date can be selected, + /// either by setting SelectedDate or the + /// first value in SelectedDates. AddRange + /// cannot be used. + /// + /// SingleRange A single range of dates can be selected. + /// Setting SelectedDate, adding a date + /// individually to SelectedDates, or using + /// AddRange will clear all previous values + /// from SelectedDates. + /// MultipleRange Multiple non-contiguous ranges of dates + /// can be selected. Adding a date + /// individually to SelectedDates or using + /// AddRange will not clear SelectedDates. + /// Setting SelectedDate will still clear + /// SelectedDates, but additional dates or + /// range can then be added. Adding a range + /// that includes some dates that are + /// already selected or overlaps with + /// another range results in the union of + /// the ranges and does not cause an + /// exception. + /// + public SelectedDatesCollection SelectedDates { get; private set; } + private static bool IsSelectionChanged(SelectionChangedEventArgs e) + { + if (e.AddedItems.Count != e.RemovedItems.Count) + { + return true; + } + foreach (DateTime addedDate in e.AddedItems) + { + if (!e.RemovedItems.Contains(addedDate)) + { + return true; + } + } + return false; + } + internal void OnSelectedDatesCollectionChanged(SelectionChangedEventArgs e) + { + if (IsSelectionChanged(e)) + { + e.RoutedEvent = SelectingItemsControl.SelectionChangedEvent; + e.Source = this; + SelectedDatesChanged?.Invoke(this, e); + } + } + + internal Collection RemovedItems { get; set; } + internal DateTime? LastSelectedDateInternal { get; set; } + internal DateTime? LastSelectedDate + { + get { return LastSelectedDateInternal; } + set + { + LastSelectedDateInternal = value; + + if (SelectionMode == CalendarSelectionMode.None) + { + if (FocusButton != null) + { + FocusButton.IsCurrent = false; + } + FocusButton = FindDayButtonFromDay(LastSelectedDate.Value); + if (FocusButton != null) + { + FocusButton.IsCurrent = HasFocusInternal; + } + } + } + } + + internal DateTime SelectedMonth + { + get { return _selectedMonth; } + set + { + int monthDifferenceStart = DateTimeHelper.CompareYearMonth(value, DisplayDateRangeStart); + int monthDifferenceEnd = DateTimeHelper.CompareYearMonth(value, DisplayDateRangeEnd); + + if (monthDifferenceStart >= 0 && monthDifferenceEnd <= 0) + { + _selectedMonth = DateTimeHelper.DiscardDayTime(value); + } + else + { + if (monthDifferenceStart < 0) + { + _selectedMonth = DateTimeHelper.DiscardDayTime(DisplayDateRangeStart); + } + else + { + Debug.Assert(monthDifferenceEnd > 0, "monthDifferenceEnd should be greater than 0!"); + _selectedMonth = DateTimeHelper.DiscardDayTime(DisplayDateRangeEnd); + } + } + } + } + internal DateTime SelectedYear + { + get { return _selectedYear; } + set + { + if (value.Year < DisplayDateRangeStart.Year) + { + _selectedYear = DisplayDateRangeStart; + } + else + { + if (value.Year > DisplayDateRangeEnd.Year) + { + _selectedYear = DisplayDateRangeEnd; + } + else + { + _selectedYear = value; + } + } + } + } + + public static readonly DirectProperty DisplayDateProperty = + AvaloniaProperty.RegisterDirect( + nameof(DisplayDate), + o => o.DisplayDate, + (o, v) => o.DisplayDate = v, + defaultBindingMode: BindingMode.TwoWay); + /// + /// Gets or sets the date to display. + /// + /// The date to display. + /// + /// The given date is not in the range specified by + /// + /// and + /// . + /// + /// + /// + /// This property allows the developer to specify a date to display. If + /// this property is a null reference (Nothing in Visual Basic), + /// SelectedDate is displayed. If SelectedDate is also a null reference + /// (Nothing in Visual Basic), Today is displayed. The default is + /// Today. + /// + /// + /// To set this property in XAML, use a date specified in the format + /// yyyy/mm/dd. The mm and dd components must always consist of two + /// characters, with a leading zero if necessary. For instance, the + /// month of May should be specified as 05. + /// + /// + public DateTime DisplayDate + { + get { return _displayDate; } + set { SetAndRaise(DisplayDateProperty, ref _displayDate, value); } + } + internal DateTime DisplayDateInternal { get; private set; } + + private void OnDisplayDateChanged(AvaloniaPropertyChangedEventArgs e) + { + UpdateDisplayDate(this, (DateTime)e.NewValue, (DateTime)e.OldValue); + } + private static void UpdateDisplayDate(Calendar c, DateTime addedDate, DateTime removedDate) + { + Debug.Assert(c != null, "c should not be null!"); + + // If DisplayDate < DisplayDateStart, DisplayDate = DisplayDateStart + if (DateTime.Compare(addedDate, c.DisplayDateRangeStart) < 0) + { + c.DisplayDate = c.DisplayDateRangeStart; + return; + } + + // If DisplayDate > DisplayDateEnd, DisplayDate = DisplayDateEnd + if (DateTime.Compare(addedDate, c.DisplayDateRangeEnd) > 0) + { + c.DisplayDate = c.DisplayDateRangeEnd; + return; + } + + c.DisplayDateInternal = DateTimeHelper.DiscardDayTime(addedDate); + c.UpdateMonths(); + c.OnDisplayDate(new CalendarDateChangedEventArgs(removedDate, addedDate)); + } + private void OnDisplayDate(CalendarDateChangedEventArgs e) + { + DisplayDateChanged?.Invoke(this, e); + } + + public static readonly DirectProperty DisplayDateStartProperty = + AvaloniaProperty.RegisterDirect( + nameof(DisplayDateStart), + o => o.DisplayDateStart, + (o, v) => o.DisplayDateStart = v, + defaultBindingMode: BindingMode.TwoWay); + /// + /// Gets or sets the first date to be displayed. + /// + /// The first date to display. + /// + /// To set this property in XAML, use a date specified in the format + /// yyyy/mm/dd. The mm and dd components must always consist of two + /// characters, with a leading zero if necessary. For instance, the + /// month of May should be specified as 05. + /// + public DateTime? DisplayDateStart + { + get { return _displayDateStart; } + set { SetAndRaise(DisplayDateStartProperty, ref _displayDateStart, value); } + } + private void OnDisplayDateStartChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!this.IsHandlerSuspended(Calendar.DisplayDateStartProperty)) + { + DateTime? newValue = e.NewValue as DateTime?; + + if (newValue.HasValue) + { + // DisplayDateStart coerces to the value of the + // SelectedDateMin if SelectedDateMin < DisplayDateStart + DateTime? selectedDateMin = SelectedDateMin(this); + + if (selectedDateMin.HasValue && DateTime.Compare(selectedDateMin.Value, newValue.Value) < 0) + { + DisplayDateStart = selectedDateMin.Value; + return; + } + + // if DisplayDateStart > DisplayDateEnd, + // DisplayDateEnd = DisplayDateStart + if (DateTime.Compare(newValue.Value, DisplayDateRangeEnd) > 0) + { + DisplayDateEnd = DisplayDateStart; + } + + // If DisplayDate < DisplayDateStart, + // DisplayDate = DisplayDateStart + if (DateTimeHelper.CompareYearMonth(newValue.Value, DisplayDateInternal) > 0) + { + DisplayDate = newValue.Value; + } + } + UpdateMonths(); + } + } + + /// + /// Gets a collection of dates that are marked as not selectable. + /// + /// + /// A collection of dates that cannot be selected. The default value is + /// an empty collection. + /// + /// + /// Adding a date to this collection when it is already selected or + /// adding a date outside the range specified by DisplayDateStart and + /// DisplayDateEnd. + /// + /// + /// + /// Dates in this collection will appear as disabled on the calendar. + /// + /// + /// To make all past dates not selectable, you can use the + /// AddDatesInPast method provided by the collection returned by this + /// property. + /// + /// + public CalendarBlackoutDatesCollection BlackoutDates { get; private set; } + + private static DateTime? SelectedDateMin(Calendar cal) + { + DateTime selectedDateMin; + + if (cal.SelectedDates.Count > 0) + { + selectedDateMin = cal.SelectedDates[0]; + Debug.Assert(DateTime.Compare(cal.SelectedDate.Value, selectedDateMin) == 0, "The SelectedDate should be the minimum selected date!"); + } + else + { + return null; + } + + foreach (DateTime selectedDate in cal.SelectedDates) + { + if (DateTime.Compare(selectedDate, selectedDateMin) < 0) + { + selectedDateMin = selectedDate; + } + } + return selectedDateMin; + } + internal DateTime DisplayDateRangeStart + { + get { return DisplayDateStart.GetValueOrDefault(DateTime.MinValue); } + } + + public static readonly DirectProperty DisplayDateEndProperty = + AvaloniaProperty.RegisterDirect( + nameof(DisplayDateEnd), + o => o.DisplayDateEnd, + (o, v) => o.DisplayDateEnd = v, + defaultBindingMode: BindingMode.TwoWay); + + /// + /// Gets or sets the last date to be displayed. + /// + /// The last date to display. + /// + /// To set this property in XAML, use a date specified in the format + /// yyyy/mm/dd. The mm and dd components must always consist of two + /// characters, with a leading zero if necessary. For instance, the + /// month of May should be specified as 05. + /// + public DateTime? DisplayDateEnd + { + get { return _displayDateEnd; } + set { SetAndRaise(DisplayDateEndProperty, ref _displayDateEnd, value); } + } + + private void OnDisplayDateEndChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!this.IsHandlerSuspended(Calendar.DisplayDateEndProperty)) + { + DateTime? newValue = e.NewValue as DateTime?; + + if (newValue.HasValue) + { + // DisplayDateEnd coerces to the value of the + // SelectedDateMax if SelectedDateMax > DisplayDateEnd + DateTime? selectedDateMax = SelectedDateMax(this); + + if (selectedDateMax.HasValue && DateTime.Compare(selectedDateMax.Value, newValue.Value) > 0) + { + DisplayDateEnd = selectedDateMax.Value; + return; + } + + // if DisplayDateEnd < DisplayDateStart, + // DisplayDateEnd = DisplayDateStart + if (DateTime.Compare(newValue.Value, DisplayDateRangeStart) < 0) + { + DisplayDateEnd = DisplayDateStart; + return; + } + + // If DisplayDate > DisplayDateEnd, + // DisplayDate = DisplayDateEnd + if (DateTimeHelper.CompareYearMonth(newValue.Value, DisplayDateInternal) < 0) + { + DisplayDate = newValue.Value; + } + } + UpdateMonths(); + } + } + + private static DateTime? SelectedDateMax(Calendar cal) + { + DateTime selectedDateMax; + + if (cal.SelectedDates.Count > 0) + { + selectedDateMax = cal.SelectedDates[0]; + Debug.Assert(DateTime.Compare(cal.SelectedDate.Value, selectedDateMax) == 0, "The SelectedDate should be the maximum SelectedDate!"); + } + else + { + return null; + } + + foreach (DateTime selectedDate in cal.SelectedDates) + { + if (DateTime.Compare(selectedDate, selectedDateMax) > 0) + { + selectedDateMax = selectedDate; + } + } + return selectedDateMax; + } + internal DateTime DisplayDateRangeEnd + { + get { return DisplayDateEnd.GetValueOrDefault(DateTime.MaxValue); } + } + + internal DateTime? HoverStart { get; set; } + internal int? HoverStartIndex { get; set; } + internal DateTime? HoverEndInternal { get; set; } + internal DateTime? HoverEnd + { + get { return HoverEndInternal; } + set + { + HoverEndInternal = value; + LastSelectedDate = value; + } + } + internal int? HoverEndIndex { get; set; } + internal bool HasFocusInternal { get; set; } + internal bool IsMouseSelection { get; set; } + + + /// + /// Gets or sets a value indicating whether DatePicker should change its + /// DisplayDate because of a SelectedDate change on its Calendar. + /// + internal bool DatePickerDisplayDateFlag { get; set; } + + internal CalendarDayButton FindDayButtonFromDay(DateTime day) + { + CalendarDayButton b; + DateTime? d; + CalendarItem monthControl = MonthControl; + + // REMOVE_RTM: should be updated if we support MultiCalendar + int count = RowsPerMonth * ColumnsPerMonth; + if (monthControl != null) + { + if (monthControl.MonthView != null) + { + for (int childIndex = ColumnsPerMonth; childIndex < count; childIndex++) + { + b = monthControl.MonthView.Children[childIndex] as CalendarDayButton; + d = b.DataContext as DateTime?; + + if (d.HasValue) + { + if (DateTimeHelper.CompareDays(d.Value, day) == 0) + { + return b; + } + } + } + } + } + return null; + } + + private void Calendar_SizeChanged(object sender, EventArgs e) + { + Debug.Assert(sender is Calendar, "The sender should be a Calendar!"); + + var size = Bounds.Size; + RectangleGeometry rg = new RectangleGeometry(); + rg.Rect = new Rect(0, 0, size.Width, size.Height); + + if (Root != null) + { + Root.Clip = rg; + } + } + + private void OnSelectedMonthChanged(DateTime? selectedMonth) + { + if (selectedMonth.HasValue) + { + Debug.Assert(DisplayMode == CalendarMode.Year, "DisplayMode should be Year!"); + SelectedMonth = selectedMonth.Value; + UpdateMonths(); + } + } + private void OnSelectedYearChanged(DateTime? selectedYear) + { + if (selectedYear.HasValue) + { + Debug.Assert(DisplayMode == CalendarMode.Decade, "DisplayMode should be Decade!"); + SelectedYear = selectedYear.Value; + UpdateMonths(); + } + } + internal void OnHeaderClick() + { + Debug.Assert(DisplayMode == CalendarMode.Year || DisplayMode == CalendarMode.Decade, "The DisplayMode should be Year or Decade"); + CalendarItem monthControl = MonthControl; + if (monthControl != null && monthControl.MonthView != null && monthControl.YearView != null) + { + monthControl.MonthView.IsVisible = false; + monthControl.YearView.IsVisible = true; + UpdateMonths(); + } + } + + internal void ResetStates() + { + CalendarDayButton d; + CalendarItem monthControl = MonthControl; + int count = RowsPerMonth * ColumnsPerMonth; + if (monthControl != null) + { + if (monthControl.MonthView != null) + { + for (int childIndex = ColumnsPerMonth; childIndex < count; childIndex++) + { + d = monthControl.MonthView.Children[childIndex] as CalendarDayButton; + d.IgnoreMouseOverState(); + } + } + } + + } + + internal void UpdateMonths() + { + CalendarItem monthControl = MonthControl; + if (monthControl != null) + { + switch (DisplayMode) + { + case CalendarMode.Month: + { + monthControl.UpdateMonthMode(); + break; + } + case CalendarMode.Year: + { + monthControl.UpdateYearMode(); + break; + } + case CalendarMode.Decade: + { + monthControl.UpdateDecadeMode(); + break; + } + } + } + } + + internal static bool IsValidDateSelection(Calendar cal, DateTime? value) + { + if (!value.HasValue) + { + return true; + } + else + { + if (cal.BlackoutDates.Contains(value.Value)) + { + return false; + } + else + { + if (DateTime.Compare(value.Value, cal.DisplayDateRangeStart) < 0) + { + cal.SetValueNoCallback(Calendar.DisplayDateStartProperty, value); + } + else if (DateTime.Compare(value.Value, cal.DisplayDateRangeEnd) > 0) + { + cal.SetValueNoCallback(Calendar.DisplayDateEndProperty, value); + } + return true; + } + } + } + private static bool IsValidKeyboardSelection(Calendar cal, DateTime? value) + { + if (!value.HasValue) + { + return true; + } + else + { + if (cal.BlackoutDates.Contains(value.Value)) + { + return false; + } + else + { + return (DateTime.Compare(value.Value, cal.DisplayDateRangeStart) >= 0 && DateTime.Compare(value.Value, cal.DisplayDateRangeEnd) <= 0); + } + } + } + + /// + /// This method highlights the days in MultiSelection mode without + /// adding them to the SelectedDates collection. + /// + internal void HighlightDays() + { + if (HoverEnd != null && HoverStart != null) + { + int startIndex, endIndex, i; + CalendarDayButton b; + DateTime? d; + CalendarItem monthControl = MonthControl; + + // This assumes a contiguous set of dates: + if (HoverEndIndex != null && HoverStartIndex != null) + { + SortHoverIndexes(out startIndex, out endIndex); + + for (i = startIndex; i <= endIndex; i++) + { + b = monthControl.MonthView.Children[i] as CalendarDayButton; + b.IsSelected = true; + d = b.DataContext as DateTime?; + + if (d.HasValue && DateTimeHelper.CompareDays(HoverEnd.Value, d.Value) == 0) + { + if (FocusButton != null) + { + FocusButton.IsCurrent = false; + } + b.IsCurrent = HasFocusInternal; + FocusButton = b; + } + } + } + } + } + /// + /// This method un-highlights the days that were hovered over but not + /// added to the SelectedDates collection or un-highlighted the + /// previously selected days in SingleRange Mode. + /// + internal void UnHighlightDays() + { + if (HoverEnd != null && HoverStart != null) + { + CalendarItem monthControl = MonthControl; + CalendarDayButton b; + DateTime? d; + + if (HoverEndIndex != null && HoverStartIndex != null) + { + int i; + SortHoverIndexes(out int startIndex, out int endIndex); + + if (SelectionMode == CalendarSelectionMode.MultipleRange) + { + for (i = startIndex; i <= endIndex; i++) + { + b = monthControl.MonthView.Children[i] as CalendarDayButton; + d = b.DataContext as DateTime?; + + if (d.HasValue) + { + if (!SelectedDates.Contains(d.Value)) + { + b.IsSelected = false; + } + } + } + } + else + { + // It is SingleRange + for (i = startIndex; i <= endIndex; i++) + { + (monthControl.MonthView.Children[i] as CalendarDayButton).IsSelected = false; + } + } + } + } + } + internal void SortHoverIndexes(out int startIndex, out int endIndex) + { + if (DateTimeHelper.CompareDays(HoverEnd.Value, HoverStart.Value) > 0) + { + startIndex = HoverStartIndex.Value; + endIndex = HoverEndIndex.Value; + } + else + { + startIndex = HoverEndIndex.Value; + endIndex = HoverStartIndex.Value; + } + } + + internal void OnPreviousClick() + { + if (DisplayMode == CalendarMode.Month && DisplayDate != null) + { + DateTime? d = DateTimeHelper.AddMonths(DateTimeHelper.DiscardDayTime(DisplayDate), -1); + if (d.HasValue) + { + if (!LastSelectedDate.HasValue || DateTimeHelper.CompareYearMonth(LastSelectedDate.Value, d.Value) != 0) + { + LastSelectedDate = d.Value; + } + DisplayDate = d.Value; + } + } + else + { + if (DisplayMode == CalendarMode.Year) + { + DateTime? d = DateTimeHelper.AddYears(new DateTime(SelectedMonth.Year, 1, 1), -1); + + if (d.HasValue) + { + SelectedMonth = d.Value; + } + else + { + SelectedMonth = DateTimeHelper.DiscardDayTime(DisplayDateRangeStart); + } + } + else + { + Debug.Assert(DisplayMode == CalendarMode.Decade, "DisplayMode should be Decade!"); + + DateTime? d = DateTimeHelper.AddYears(new DateTime(SelectedYear.Year, 1, 1), -10); + + if (d.HasValue) + { + int decade = Math.Max(1, DateTimeHelper.DecadeOfDate(d.Value)); + SelectedYear = new DateTime(decade, 1, 1); + } + else + { + SelectedYear = DateTimeHelper.DiscardDayTime(DisplayDateRangeStart); + } + } + UpdateMonths(); + } + } + internal void OnNextClick() + { + if (DisplayMode == CalendarMode.Month && DisplayDate != null) + { + DateTime? d = DateTimeHelper.AddMonths(DateTimeHelper.DiscardDayTime(DisplayDate), 1); + if (d.HasValue) + { + if (!LastSelectedDate.HasValue || DateTimeHelper.CompareYearMonth(LastSelectedDate.Value, d.Value) != 0) + { + LastSelectedDate = d.Value; + } + DisplayDate = d.Value; + } + } + else + { + if (DisplayMode == CalendarMode.Year) + { + DateTime? d = DateTimeHelper.AddYears(new DateTime(SelectedMonth.Year, 1, 1), 1); + + if (d.HasValue) + { + SelectedMonth = d.Value; + } + else + { + SelectedMonth = DateTimeHelper.DiscardDayTime(DisplayDateRangeEnd); + } + } + else + { + Debug.Assert(DisplayMode == CalendarMode.Decade, "DisplayMode should be Decade"); + + DateTime? d = DateTimeHelper.AddYears(new DateTime(SelectedYear.Year, 1, 1), 10); + + if (d.HasValue) + { + int decade = Math.Max(1, DateTimeHelper.DecadeOfDate(d.Value)); + SelectedYear = new DateTime(decade, 1, 1); + } + else + { + SelectedYear = DateTimeHelper.DiscardDayTime(DisplayDateRangeEnd); + } + } + UpdateMonths(); + } + } + + /// + /// If the day is a trailing day, Update the DisplayDate. + /// + /// Inherited code: Requires comment. + internal void OnDayClick(DateTime selectedDate) + { + Debug.Assert(DisplayMode == CalendarMode.Month, "DisplayMode should be Month!"); + int i = DateTimeHelper.CompareYearMonth(selectedDate, DisplayDateInternal); + + if (SelectionMode == CalendarSelectionMode.None) + { + LastSelectedDate = selectedDate; + } + + if (i > 0) + { + OnNextClick(); + } + else if (i < 0) + { + OnPreviousClick(); + } + } + private void OnMonthClick() + { + CalendarItem monthControl = MonthControl; + if (monthControl != null && monthControl.YearView != null && monthControl.MonthView != null) + { + monthControl.YearView.IsVisible = false; + monthControl.MonthView.IsVisible = true; + + if (!LastSelectedDate.HasValue || DateTimeHelper.CompareYearMonth(LastSelectedDate.Value, DisplayDate) != 0) + { + LastSelectedDate = DisplayDate; + } + + UpdateMonths(); + } + } + + public override string ToString() + { + if (SelectedDate != null) + { + return SelectedDate.Value.ToString(DateTimeHelper.GetCurrentDateFormat()); + } + else + { + return string.Empty; + } + } + + public event EventHandler SelectedDatesChanged; + + /// + /// Occurs when the + /// + /// property is changed. + /// + /// + /// This event occurs after DisplayDate is assigned its new value. + /// + public event EventHandler DisplayDateChanged; + + /// + /// Occurs when the + /// + /// property is changed. + /// + public event EventHandler DisplayModeChanged; + + /// + /// Inherited code: Requires comment. + /// + internal event EventHandler DayButtonMouseUp; + + /// + /// This method adds the days that were selected by Keyboard to the + /// SelectedDays Collection. + /// + private void AddSelection() + { + if (HoverEnd != null && HoverStart != null) + { + foreach (DateTime item in SelectedDates) + { + RemovedItems.Add(item); + } + + SelectedDates.ClearInternal(); + // In keyboard selection, we are sure that the collection does + // not include any blackout days + SelectedDates.AddRange(HoverStart.Value, HoverEnd.Value); + } + } + private void ProcessSelection(bool shift, DateTime? lastSelectedDate, int? index) + { + if (SelectionMode == CalendarSelectionMode.None && lastSelectedDate != null) + { + OnDayClick(lastSelectedDate.Value); + return; + } + if (lastSelectedDate != null && IsValidKeyboardSelection(this, lastSelectedDate.Value)) + { + if (SelectionMode == CalendarSelectionMode.SingleRange || SelectionMode == CalendarSelectionMode.MultipleRange) + { + foreach (DateTime item in SelectedDates) + { + RemovedItems.Add(item); + } + SelectedDates.ClearInternal(); + if (shift) + { + CalendarDayButton b; + _isShiftPressed = true; + if (HoverStart == null) + { + if (LastSelectedDate != null) + { + HoverStart = LastSelectedDate; + } + else + { + if (DateTimeHelper.CompareYearMonth(DisplayDateInternal, DateTime.Today) == 0) + { + HoverStart = DateTime.Today; + } + else + { + HoverStart = DisplayDateInternal; + } + } + + b = FindDayButtonFromDay(HoverStart.Value); + if (b != null) + { + HoverStartIndex = b.Index; + } + } + // the index of the SelectedDate is always the last + // selectedDate's index + UnHighlightDays(); + // If we hit a BlackOutDay with keyboard we do not + // update the HoverEnd + CalendarDateRange range; + + if (DateTime.Compare(HoverStart.Value, lastSelectedDate.Value) < 0) + { + range = new CalendarDateRange(HoverStart.Value, lastSelectedDate.Value); + } + else + { + range = new CalendarDateRange(lastSelectedDate.Value, HoverStart.Value); + } + + if (!BlackoutDates.ContainsAny(range)) + { + HoverEnd = lastSelectedDate; + + if (index.HasValue) + { + HoverEndIndex += index; + } + else + { + // For Home, End, PageUp and PageDown Keys there + // is no easy way to predict the index value + b = FindDayButtonFromDay(HoverEndInternal.Value); + + if (b != null) + { + HoverEndIndex = b.Index; + } + } + } + + OnDayClick(HoverEnd.Value); + HighlightDays(); + } + else + { + HoverStart = lastSelectedDate; + HoverEnd = lastSelectedDate; + AddSelection(); + OnDayClick(lastSelectedDate.Value); + } + } + else + { + // ON CLEAR + LastSelectedDate = lastSelectedDate.Value; + if (SelectedDates.Count > 0) + { + SelectedDates[0] = lastSelectedDate.Value; + } + else + { + SelectedDates.Add(lastSelectedDate.Value); + } + OnDayClick(lastSelectedDate.Value); + } + } + } + + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + if (!HasFocusInternal && e.MouseButton == MouseButton.Left) + { + FocusManager.Instance.Focus(this); + } + } + + internal void OnDayButtonMouseUp(PointerReleasedEventArgs e) + { + DayButtonMouseUp?.Invoke(this, e); + } + + /// + /// Default mouse wheel handler for the calendar control. + /// + /// Mouse wheel event args. + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + base.OnPointerWheelChanged(e); + if (!e.Handled) + { + CalendarExtensions.GetMetaKeyState(e.InputModifiers, out bool ctrl, out bool shift); + + if (!ctrl) + { + if (e.Delta.Y > 0) + { + ProcessPageUpKey(false); + } + else + { + ProcessPageDownKey(false); + } + } + else + { + if (e.Delta.Y > 0) + { + ProcessDownKey(ctrl, shift); + } + else + { + ProcessUpKey(ctrl, shift); + } + } + e.Handled = true; + } + } + internal void Calendar_KeyDown(object sender, KeyEventArgs e) + { + Calendar c = sender as Calendar; + Debug.Assert(c != null, "c should not be null!"); + + if (!e.Handled && c.IsEnabled) + { + e.Handled = ProcessCalendarKey(e); + } + } + internal bool ProcessCalendarKey(KeyEventArgs e) + { + if (DisplayMode == CalendarMode.Month) + { + if (LastSelectedDate.HasValue && DisplayDateInternal != null) + { + // If a blackout day is inactive, when clicked on it, the + // previous inactive day which is not a blackout day can get + // the focus. In this case we should allow keyboard + // functions on that inactive day + if (DateTimeHelper.CompareYearMonth(LastSelectedDate.Value, DisplayDateInternal) != 0 && FocusButton != null && !FocusButton.IsInactive) + { + return true; + } + } + } + + // Some keys (e.g. Left/Right) need to be translated in RightToLeft mode + Key invariantKey = e.Key; //InteractionHelper.GetLogicalKey(FlowDirection, e.Key); + + CalendarExtensions.GetMetaKeyState(e.Modifiers, out bool ctrl, out bool shift); + + switch (invariantKey) + { + case Key.Up: + { + ProcessUpKey(ctrl, shift); + return true; + } + case Key.Down: + { + ProcessDownKey(ctrl, shift); + return true; + } + case Key.Left: + { + ProcessLeftKey(shift); + return true; + } + case Key.Right: + { + ProcessRightKey(shift); + return true; + } + case Key.PageDown: + { + ProcessPageDownKey(shift); + return true; + } + case Key.PageUp: + { + ProcessPageUpKey(shift); + return true; + } + case Key.Home: + { + ProcessHomeKey(shift); + return true; + } + case Key.End: + { + ProcessEndKey(shift); + return true; + } + case Key.Enter: + case Key.Space: + { + return ProcessEnterKey(); + } + } + return false; + } + internal void ProcessUpKey(bool ctrl, bool shift) + { + switch (DisplayMode) + { + case CalendarMode.Month: + { + if (ctrl) + { + SelectedMonth = DisplayDateInternal; + DisplayMode = CalendarMode.Year; + } + else + { + DateTime? selectedDate = DateTimeHelper.AddDays(LastSelectedDate.GetValueOrDefault(DateTime.Today), -ColumnsPerMonth); + ProcessSelection(shift, selectedDate, -ColumnsPerMonth); + } + break; + } + case CalendarMode.Year: + { + if (ctrl) + { + SelectedYear = SelectedMonth; + DisplayMode = CalendarMode.Decade; + } + else + { + DateTime? selectedMonth = DateTimeHelper.AddMonths(_selectedMonth, -ColumnsPerYear); + OnSelectedMonthChanged(selectedMonth); + } + break; + } + case CalendarMode.Decade: + { + if (!ctrl) + { + DateTime? selectedYear = DateTimeHelper.AddYears(SelectedYear, -ColumnsPerYear); + OnSelectedYearChanged(selectedYear); + } + break; + } + } + } + internal void ProcessDownKey(bool ctrl, bool shift) + { + switch (DisplayMode) + { + case CalendarMode.Month: + { + if (!ctrl || shift) + { + DateTime? selectedDate = DateTimeHelper.AddDays(LastSelectedDate.GetValueOrDefault(DateTime.Today), ColumnsPerMonth); + ProcessSelection(shift, selectedDate, ColumnsPerMonth); + } + break; + } + case CalendarMode.Year: + { + if (ctrl) + { + DisplayDate = SelectedMonth; + DisplayMode = CalendarMode.Month; + } + else + { + DateTime? selectedMonth = DateTimeHelper.AddMonths(_selectedMonth, ColumnsPerYear); + OnSelectedMonthChanged(selectedMonth); + } + break; + } + case CalendarMode.Decade: + { + if (ctrl) + { + SelectedMonth = SelectedYear; + DisplayMode = CalendarMode.Year; + } + else + { + DateTime? selectedYear = DateTimeHelper.AddYears(SelectedYear, ColumnsPerYear); + OnSelectedYearChanged(selectedYear); + } + break; + } + } + } + internal void ProcessLeftKey(bool shift) + { + switch (DisplayMode) + { + case CalendarMode.Month: + { + DateTime? selectedDate = DateTimeHelper.AddDays(LastSelectedDate.GetValueOrDefault(DateTime.Today), -1); + ProcessSelection(shift, selectedDate, -1); + break; + } + case CalendarMode.Year: + { + DateTime? selectedMonth = DateTimeHelper.AddMonths(_selectedMonth, -1); + OnSelectedMonthChanged(selectedMonth); + break; + } + case CalendarMode.Decade: + { + DateTime? selectedYear = DateTimeHelper.AddYears(SelectedYear, -1); + OnSelectedYearChanged(selectedYear); + break; + } + } + } + internal void ProcessRightKey(bool shift) + { + switch (DisplayMode) + { + case CalendarMode.Month: + { + DateTime? selectedDate = DateTimeHelper.AddDays(LastSelectedDate.GetValueOrDefault(DateTime.Today), 1); + ProcessSelection(shift, selectedDate, 1); + break; + } + case CalendarMode.Year: + { + DateTime? selectedMonth = DateTimeHelper.AddMonths(_selectedMonth, 1); + OnSelectedMonthChanged(selectedMonth); + break; + } + case CalendarMode.Decade: + { + DateTime? selectedYear = DateTimeHelper.AddYears(SelectedYear, 1); + OnSelectedYearChanged(selectedYear); + break; + } + } + } + private bool ProcessEnterKey() + { + switch (DisplayMode) + { + case CalendarMode.Year: + { + DisplayDate = SelectedMonth; + DisplayMode = CalendarMode.Month; + return true; + } + case CalendarMode.Decade: + { + SelectedMonth = SelectedYear; + DisplayMode = CalendarMode.Year; + return true; + } + } + return false; + } + internal void ProcessHomeKey(bool shift) + { + switch (DisplayMode) + { + case CalendarMode.Month: + { + // REMOVE_RTM: Not all types of calendars start with Day1. If Non-Gregorian is supported check this: + DateTime? selectedDate = new DateTime(DisplayDateInternal.Year, DisplayDateInternal.Month, 1); + ProcessSelection(shift, selectedDate, null); + break; + } + case CalendarMode.Year: + { + DateTime selectedMonth = new DateTime(_selectedMonth.Year, 1, 1); + OnSelectedMonthChanged(selectedMonth); + break; + } + case CalendarMode.Decade: + { + DateTime? selectedYear = new DateTime(DateTimeHelper.DecadeOfDate(SelectedYear), 1, 1); + OnSelectedYearChanged(selectedYear); + break; + } + } + } + internal void ProcessEndKey(bool shift) + { + switch (DisplayMode) + { + case CalendarMode.Month: + { + if (DisplayDate != null) + { + DateTime? selectedDate = new DateTime(DisplayDateInternal.Year, DisplayDateInternal.Month, 1); + + if (DateTimeHelper.CompareYearMonth(DateTime.MaxValue, selectedDate.Value) > 0) + { + // since DisplayDate is not equal to + // DateTime.MaxValue we are sure selectedDate is\ + // not null + selectedDate = DateTimeHelper.AddMonths(selectedDate.Value, 1).Value; + selectedDate = DateTimeHelper.AddDays(selectedDate.Value, -1).Value; + } + else + { + selectedDate = DateTime.MaxValue; + } + ProcessSelection(shift, selectedDate, null); + } + break; + } + case CalendarMode.Year: + { + DateTime selectedMonth = new DateTime(_selectedMonth.Year, 12, 1); + OnSelectedMonthChanged(selectedMonth); + break; + } + case CalendarMode.Decade: + { + DateTime? selectedYear = new DateTime(DateTimeHelper.EndOfDecade(SelectedYear), 1, 1); + OnSelectedYearChanged(selectedYear); + break; + } + } + } + internal void ProcessPageDownKey(bool shift) + { + switch (DisplayMode) + { + case CalendarMode.Month: + { + DateTime? selectedDate = DateTimeHelper.AddMonths(LastSelectedDate.GetValueOrDefault(DateTime.Today), 1); + ProcessSelection(shift, selectedDate, null); + break; + } + case CalendarMode.Year: + { + DateTime? selectedMonth = DateTimeHelper.AddYears(_selectedMonth, 1); + OnSelectedMonthChanged(selectedMonth); + break; + } + case CalendarMode.Decade: + { + DateTime? selectedYear = DateTimeHelper.AddYears(SelectedYear, 10); + OnSelectedYearChanged(selectedYear); + break; + } + } + } + internal void ProcessPageUpKey(bool shift) + { + switch (DisplayMode) + { + case CalendarMode.Month: + { + DateTime? selectedDate = DateTimeHelper.AddMonths(LastSelectedDate.GetValueOrDefault(DateTime.Today), -1); + ProcessSelection(shift, selectedDate, null); + break; + } + case CalendarMode.Year: + { + DateTime? selectedMonth = DateTimeHelper.AddYears(_selectedMonth, -1); + OnSelectedMonthChanged(selectedMonth); + break; + } + case CalendarMode.Decade: + { + DateTime? selectedYear = DateTimeHelper.AddYears(SelectedYear, -10); + OnSelectedYearChanged(selectedYear); + break; + } + } + } + private void Calendar_KeyUp(object sender, KeyEventArgs e) + { + if (!e.Handled && (e.Key == Key.LeftShift || e.Key == Key.RightShift)) + { + ProcessShiftKeyUp(); + } + } + internal void ProcessShiftKeyUp() + { + if (_isShiftPressed && (SelectionMode == CalendarSelectionMode.SingleRange || SelectionMode == CalendarSelectionMode.MultipleRange)) + { + AddSelection(); + _isShiftPressed = false; + } + } + + protected override void OnGotFocus(GotFocusEventArgs e) + { + base.OnGotFocus(e); + HasFocusInternal = true; + + switch (DisplayMode) + { + case CalendarMode.Month: + { + DateTime focusDate; + if (LastSelectedDate.HasValue && DateTimeHelper.CompareYearMonth(DisplayDateInternal, LastSelectedDate.Value) == 0) + { + focusDate = LastSelectedDate.Value; + } + else + { + focusDate = DisplayDate; + LastSelectedDate = DisplayDate; + } + Debug.Assert(focusDate != null, "focusDate should not be null!"); + FocusButton = FindDayButtonFromDay(focusDate); + + if (FocusButton != null) + { + FocusButton.IsCurrent = true; + } + break; + } + case CalendarMode.Year: + case CalendarMode.Decade: + { + if (this.FocusCalendarButton != null) + { + FocusCalendarButton.IsCalendarButtonFocused = true; + } + break; + } + } + } + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + HasFocusInternal = false; + + switch (DisplayMode) + { + case CalendarMode.Month: + { + if (FocusButton != null) + { + FocusButton.IsCurrent = false; + } + break; + } + case CalendarMode.Year: + case CalendarMode.Decade: + { + if (FocusCalendarButton != null) + { + FocusCalendarButton.IsCalendarButtonFocused = false; + } + break; + } + } + } + /// + /// Called when the IsEnabled property changes. + /// + /// Property changed args. + private void OnIsEnabledChanged(AvaloniaPropertyChangedEventArgs e) + { + Debug.Assert(e.NewValue is bool, "NewValue should be a boolean!"); + bool isEnabled = (bool)e.NewValue; + + if (MonthControl != null) + { + MonthControl.UpdateDisabled(isEnabled); + } + } + + static Calendar() + { + IsEnabledProperty.Changed.AddClassHandler(x => x.OnIsEnabledChanged); + FirstDayOfWeekProperty.Changed.AddClassHandler(x => x.OnFirstDayOfWeekChanged); + IsTodayHighlightedProperty.Changed.AddClassHandler(x => x.OnIsTodayHighlightedChanged); + DisplayModeProperty.Changed.AddClassHandler(x => x.OnDisplayModePropertyChanged); + SelectionModeProperty.Changed.AddClassHandler(x => x.OnSelectionModeChanged); + SelectedDateProperty.Changed.AddClassHandler(x => x.OnSelectedDateChanged); + DisplayDateProperty.Changed.AddClassHandler(x => x.OnDisplayDateChanged); + DisplayDateStartProperty.Changed.AddClassHandler(x => x.OnDisplayDateStartChanged); + DisplayDateEndProperty.Changed.AddClassHandler(x => x.OnDisplayDateEndChanged); + } + + /// + /// Initializes a new instance of the + /// class. + /// + public Calendar() + { + UpdateDisplayDate(this, this.DisplayDate, DateTime.MinValue); + BlackoutDates = new CalendarBlackoutDatesCollection(this); + SelectedDates = new SelectedDatesCollection(this); + RemovedItems = new Collection(); + } + + private const string PART_ElementRoot = "Root"; + private const string PART_ElementMonth = "CalendarItem"; + /// + /// Builds the visual tree for the + /// when a new + /// template is applied. + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + + Root = e.NameScope.Find(PART_ElementRoot); + + SelectedMonth = DisplayDate; + SelectedYear = DisplayDate; + + if (Root != null) + { + CalendarItem month = e.NameScope.Find(PART_ElementMonth); + + if (month != null) + { + month.Owner = this; + } + } + + LayoutUpdated += Calendar_SizeChanged; + KeyDown += Calendar_KeyDown; + KeyUp += Calendar_KeyUp; + } + + } +} diff --git a/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs new file mode 100644 index 0000000000..60fdde3e2d --- /dev/null +++ b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs @@ -0,0 +1,223 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading; + +namespace Avalonia.Controls.Primitives +{ + public sealed class CalendarBlackoutDatesCollection : ObservableCollection + { + /// + /// The Calendar whose dates this object represents. + /// + private Calendar _owner; + + /// + /// The dispatcher thread. + /// + private Thread _dispatcherThread; + + /// + /// Initializes a new instance of the + /// + /// class. + /// + /// + /// The whose dates + /// this object represents. + /// + public CalendarBlackoutDatesCollection(Calendar owner) + { + _owner = owner ?? throw new ArgumentNullException(nameof(owner)); + _dispatcherThread = Thread.CurrentThread; + } + + /// + /// Adds all dates before to the + /// collection. + /// + public void AddDatesInPast() + { + Add(new CalendarDateRange(DateTime.MinValue, DateTime.Today.AddDays(-1))); + } + + /// + /// Returns a value that represents whether this collection contains the + /// specified date. + /// + /// The date to search for. + /// + /// True if the collection contains the specified date; otherwise, + /// false. + /// + public bool Contains(DateTime date) + { + int count = Count; + for (int i = 0; i < count; i++) + { + if (DateTimeHelper.InRange(date, this[i])) + { + return true; + } + } + return false; + } + + /// + /// Returns a value that represents whether this collection contains the + /// specified range of dates. + /// + /// The start of the date range. + /// The end of the date range. + /// + /// True if all dates in the range are contained in the collection; + /// otherwise, false. + /// + public bool Contains(DateTime start, DateTime end) + { + DateTime rangeStart; + DateTime rangeEnd; + + if (DateTime.Compare(end, start) > -1) + { + rangeStart = DateTimeHelper.DiscardTime(start).Value; + rangeEnd = DateTimeHelper.DiscardTime(end).Value; + } + else + { + rangeStart = DateTimeHelper.DiscardTime(end).Value; + rangeEnd = DateTimeHelper.DiscardTime(start).Value; + } + + int count = Count; + for (int i = 0; i < count; i++) + { + CalendarDateRange range = this[i]; + if (DateTime.Compare(range.Start, rangeStart) == 0 && DateTime.Compare(range.End, rangeEnd) == 0) + { + return true; + } + } + return false; + } + + /// + /// Returns a value that represents whether this collection contains any + /// date in the specified range. + /// + /// The range of dates to search for. + /// + /// True if any date in the range is contained in the collection; + /// otherwise, false. + /// + public bool ContainsAny(CalendarDateRange range) + { + return this.Any(r => r.ContainsAny(range)); + } + + /// + /// Removes all items from the collection. + /// + /// + /// This implementation raises the CollectionChanged event. + /// + protected override void ClearItems() + { + EnsureValidThread(); + + base.ClearItems(); + _owner.UpdateMonths(); + } + + /// + /// Inserts an item into the collection at the specified index. + /// + /// + /// The zero-based index at which item should be inserted. + /// + /// The object to insert. + /// + /// This implementation raises the CollectionChanged event. + /// + protected override void InsertItem(int index, CalendarDateRange item) + { + EnsureValidThread(); + + if (!IsValid(item)) + { + throw new ArgumentOutOfRangeException("Value is not valid."); + } + + base.InsertItem(index, item); + _owner.UpdateMonths(); + } + + /// + /// Removes the item at the specified index of the collection. + /// + /// + /// The zero-based index of the element to remove. + /// + /// + /// This implementation raises the CollectionChanged event. + /// + protected override void RemoveItem(int index) + { + EnsureValidThread(); + + base.RemoveItem(index); + _owner.UpdateMonths(); + } + + /// + /// Replaces the element at the specified index. + /// + /// + /// The zero-based index of the element to replace. + /// + /// + /// The new value for the element at the specified index. + /// + /// + /// This implementation raises the CollectionChanged event. + /// + protected override void SetItem(int index, CalendarDateRange item) + { + EnsureValidThread(); + + if (!IsValid(item)) + { + throw new ArgumentOutOfRangeException("Value is not valid."); + } + + base.SetItem(index, item); + _owner.UpdateMonths(); + } + + private bool IsValid(CalendarDateRange item) + { + foreach (DateTime day in _owner.SelectedDates) + { + if (DateTimeHelper.InRange(day, item)) + { + return false; + } + } + + return true; + } + + private void EnsureValidThread() + { + if (Thread.CurrentThread != _dispatcherThread) + { + throw new NotSupportedException("This type of Collection does not support changes to its SourceCollection from a thread different from the Dispatcher thread."); + } + } + } +} diff --git a/src/Avalonia.Controls/Calendar/CalendarButton.cs b/src/Avalonia.Controls/Calendar/CalendarButton.cs new file mode 100644 index 0000000000..224d9b782b --- /dev/null +++ b/src/Avalonia.Controls/Calendar/CalendarButton.cs @@ -0,0 +1,193 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Input; +using System; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Represents a button on a + /// . + /// + public sealed class CalendarButton : Button + { + /// + /// A value indicating whether the button is focused. + /// + private bool _isCalendarButtonFocused; + + /// + /// A value indicating whether the button is inactive. + /// + private bool _isInactive; + + /// + /// A value indicating whether the button is selected. + /// + private bool _isSelected; + + /// + /// Initializes a new instance of the + /// + /// class. + /// + public CalendarButton() + : base() + { + Content = DateTimeHelper.GetCurrentDateFormat().AbbreviatedMonthNames[0]; + } + + /// + /// Gets or sets the Calendar associated with this button. + /// + internal Calendar Owner { get; set; } + + /// + /// Gets or sets a value indicating whether the button is focused. + /// + internal bool IsCalendarButtonFocused + { + get { return _isCalendarButtonFocused; } + set + { + if (_isCalendarButtonFocused != value) + { + _isCalendarButtonFocused = value; + SetPseudoClasses(); + } + } + } + + /// + /// Gets or sets a value indicating whether the button is inactive. + /// + internal bool IsInactive + { + get { return _isInactive; } + set + { + if (_isInactive != value) + { + _isInactive = value; + SetPseudoClasses(); + } + } + } + + /// + /// Gets or sets a value indicating whether the button is selected. + /// + internal bool IsSelected + { + get { return _isSelected; } + set + { + if (_isSelected != value) + { + _isSelected = value; + SetPseudoClasses(); + } + } + } + + /// + /// Builds the visual tree for the + /// + /// when a new template is applied. + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + SetPseudoClasses(); + } + + /// + /// Sets PseudoClasses based on current state. + /// + private void SetPseudoClasses() + { + PseudoClasses.Set(":selected", IsSelected); + PseudoClasses.Set(":inactive", IsInactive); + PseudoClasses.Set(":btnfocused", IsCalendarButtonFocused && IsEnabled); + } + + /// + /// Occurs when the left mouse button is pressed (or when the tip of the + /// stylus touches the tablet PC) while the mouse pointer is over a + /// UIElement. + /// + public event EventHandler CalendarLeftMouseButtonDown; + + /// + /// Occurs when the left mouse button is released (or the tip of the + /// stylus is removed from the tablet PC) while the mouse (or the + /// stylus) is over a UIElement (or while a UIElement holds mouse + /// capture). + /// + public event EventHandler CalendarLeftMouseButtonUp; + + /// + /// Provides class handling for the MouseLeftButtonDown event that + /// occurs when the left mouse button is pressed while the mouse pointer + /// is over this control. + /// + /// The event data. + /// + /// e is a null reference (Nothing in Visual Basic). + /// + /// + /// This method marks the MouseLeftButtonDown event as handled by + /// setting the MouseButtonEventArgs.Handled property of the event data + /// to true when the button is enabled and its ClickMode is not set to + /// Hover. Since this method marks the MouseLeftButtonDown event as + /// handled in some situations, you should use the Click event instead + /// to detect a button click. + /// + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + if (e.MouseButton == MouseButton.Left) + CalendarLeftMouseButtonDown?.Invoke(this, e); + } + + /// + /// Provides handling for the MouseLeftButtonUp event that occurs when + /// the left mouse button is released while the mouse pointer is over + /// this control. + /// + /// The event data. + /// + /// e is a null reference (Nothing in Visual Basic). + /// + /// + /// This method marks the MouseLeftButtonUp event as handled by setting + /// the MouseButtonEventArgs.Handled property of the event data to true + /// when the button is enabled and its ClickMode is not set to Hover. + /// Since this method marks the MouseLeftButtonUp event as handled in + /// some situations, you should use the Click event instead to detect a + /// button click. + /// + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + if (e.MouseButton == MouseButton.Left) + CalendarLeftMouseButtonUp?.Invoke(this, e); + } + + /// + /// We need to simulate the MouseLeftButtonUp event for the + /// CalendarButton that stays in Pressed state after MouseCapture is + /// released since there is no actual MouseLeftButtonUp event for the + /// release. + /// + /// Event arguments. + internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e) + { + e.Handled = false; + base.OnPointerReleased(e); + } + } +} diff --git a/src/Avalonia.Controls/Calendar/CalendarDateRange.cs b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs new file mode 100644 index 0000000000..273cda8c5b --- /dev/null +++ b/src/Avalonia.Controls/Calendar/CalendarDateRange.cs @@ -0,0 +1,79 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Diagnostics; + +namespace Avalonia.Controls +{ + public sealed class CalendarDateRange + { + /// + /// Gets the first date in the represented range. + /// + /// The first date in the represented range. + public DateTime Start { get; private set; } + + /// + /// Gets the last date in the represented range. + /// + /// The last date in the represented range. + public DateTime End { get; private set; } + + /// + /// Initializes a new instance of the + /// class + /// with a single date. + /// + /// The date to be represented by the range. + public CalendarDateRange(DateTime day) + { + Start = day; + End = day; + } + + /// + /// Initializes a new instance of the + /// class + /// with a range of dates. + /// + /// + /// The start of the range to be represented. + /// + /// The end of the range to be represented. + public CalendarDateRange(DateTime start, DateTime end) + { + if (DateTime.Compare(end, start) >= 0) + { + Start = start; + End = end; + } + else + { + // Always use the start for ranges on the same day + Start = start; + End = start; + } + } + + /// + /// Returns true if any day in the given DateTime range is contained in + /// the current CalendarDateRange. + /// + /// Inherited code: Requires comment 1. + /// Inherited code: Requires comment 2. + internal bool ContainsAny(CalendarDateRange range) + { + Debug.Assert(range != null, "range should not be null!"); + + int start = DateTime.Compare(Start, range.Start); + + // Check if any part of the supplied range is contained by this + // range or if the supplied range completely covers this range. + return (start <= 0 && DateTime.Compare(End, range.Start) >= 0) || + (start >= 0 && DateTime.Compare(Start, range.End) <= 0); + } + } +} diff --git a/src/Avalonia.Controls/Calendar/CalendarDayButton.cs b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs new file mode 100644 index 0000000000..f6d0fbba62 --- /dev/null +++ b/src/Avalonia.Controls/Calendar/CalendarDayButton.cs @@ -0,0 +1,253 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Input; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; + +namespace Avalonia.Controls.Primitives +{ + public sealed class CalendarDayButton : Button + { + /// + /// Default content for the CalendarDayButton. + /// + private const int DefaultContent = 1; + + private bool _isCurrent; + private bool _ignoringMouseOverState; + private bool _isBlackout; + private bool _isToday; + private bool _isInactive; + private bool _isSelected; + + /// + /// Initializes a new instance of the + /// + /// class. + /// + public CalendarDayButton() + : base() + { + //Focusable = false; + Content = DefaultContent.ToString(CultureInfo.CurrentCulture); + } + + /// + /// Gets or sets the Calendar associated with this button. + /// + internal Calendar Owner { get; set; } + internal int Index { get; set; } + + /// + /// Gets or sets a value indicating whether the button is the focused + /// element on the Calendar control. + /// + internal bool IsCurrent + { + get { return _isCurrent; } + set + { + if (_isCurrent != value) + { + _isCurrent = value; + SetPseudoClasses(); + } + } + } + + /// + /// Ensure the button is not in the MouseOver state. + /// + /// + /// If a button is in the MouseOver state when a Popup is closed (as is + /// the case when you select a date in the DatePicker control), it will + /// continue to think it's in the mouse over state even when the Popup + /// opens again and it's not. This method is used to forcibly clear the + /// state by changing the CommonStates state group. + /// + internal void IgnoreMouseOverState() + { + // TODO: Investigate whether this needs to be done by changing the + // state everytime we change any state, or if it can be done once + // to properly reset the control. + + _ignoringMouseOverState = false; + + // If the button thinks it's in the MouseOver state (which can + // happen when a Popup is closed before the button can change state) + // we will override the state so it shows up as normal. + if (IsPointerOver) + { + _ignoringMouseOverState = true; + SetPseudoClasses(); + } + } + + /// + /// Gets or sets a value indicating whether this is a blackout date. + /// + internal bool IsBlackout + { + get { return _isBlackout; } + set + { + if (_isBlackout != value) + { + _isBlackout = value; + SetPseudoClasses(); + } + } + } + + /// + /// Gets or sets a value indicating whether this button represents + /// today. + /// + internal bool IsToday + { + get { return _isToday; } + set + { + if (_isToday != value) + { + _isToday = value; + SetPseudoClasses(); + } + } + } + /// + /// Gets or sets a value indicating whether the button is inactive. + /// + internal bool IsInactive + { + get { return _isInactive; } + set + { + if (_isInactive != value) + { + _isInactive = value; + SetPseudoClasses(); + } + } + } + + /// + /// Gets or sets a value indicating whether the button is selected. + /// + internal bool IsSelected + { + get { return _isSelected; } + set + { + if (_isSelected != value) + { + _isSelected = value; + SetPseudoClasses(); + } + } + } + + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + SetPseudoClasses(); + } + private void SetPseudoClasses() + { + if (_ignoringMouseOverState) + { + PseudoClasses.Set(":pressed", IsPressed); + PseudoClasses.Set(":disabled", !IsEnabled); + } + + PseudoClasses.Set(":selected", IsSelected); + PseudoClasses.Set(":inactive", IsInactive); + PseudoClasses.Set(":today", IsToday); + PseudoClasses.Set(":blackout", IsBlackout); + PseudoClasses.Set(":dayfocused", IsCurrent && IsEnabled); + } + + /// + /// Occurs when the left mouse button is pressed (or when the tip of the + /// stylus touches the tablet PC) while the mouse pointer is over a + /// UIElement. + /// + public event EventHandler CalendarDayButtonMouseDown; + + /// + /// Occurs when the left mouse button is released (or the tip of the + /// stylus is removed from the tablet PC) while the mouse (or the + /// stylus) is over a UIElement (or while a UIElement holds mouse + /// capture). + /// + public event EventHandler CalendarDayButtonMouseUp; + + /// + /// Provides class handling for the MouseLeftButtonDown event that + /// occurs when the left mouse button is pressed while the mouse pointer + /// is over this control. + /// + /// The event data. + /// + /// e is a null reference (Nothing in Visual Basic). + /// + /// + /// This method marks the MouseLeftButtonDown event as handled by + /// setting the MouseButtonEventArgs.Handled property of the event data + /// to true when the button is enabled and its ClickMode is not set to + /// Hover. Since this method marks the MouseLeftButtonDown event as + /// handled in some situations, you should use the Click event instead + /// to detect a button click. + /// + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + base.OnPointerPressed(e); + + if (e.MouseButton == MouseButton.Left) + CalendarDayButtonMouseDown?.Invoke(this, e); + } + + /// + /// Provides handling for the MouseLeftButtonUp event that occurs when + /// the left mouse button is released while the mouse pointer is over + /// this control. + /// + /// The event data. + /// + /// e is a null reference (Nothing in Visual Basic). + /// + /// + /// This method marks the MouseLeftButtonUp event as handled by setting + /// the MouseButtonEventArgs.Handled property of the event data to true + /// when the button is enabled and its ClickMode is not set to Hover. + /// Since this method marks the MouseLeftButtonUp event as handled in + /// some situations, you should use the Click event instead to detect a + /// button click. + /// + protected override void OnPointerReleased(PointerReleasedEventArgs e) + { + base.OnPointerReleased(e); + + if (e.MouseButton == MouseButton.Left) + CalendarDayButtonMouseUp?.Invoke(this, e); + } + + /// + /// We need to simulate the MouseLeftButtonUp event for the + /// CalendarDayButton that stays in Pressed state after MouseCapture is + /// released since there is no actual MouseLeftButtonUp event for the + /// release. + /// + /// Event arguments. + internal void SendMouseLeftButtonUp(PointerReleasedEventArgs e) + { + e.Handled = false; + base.OnPointerReleased(e); + } + } +} diff --git a/src/Avalonia.Controls/Calendar/CalendarExtensions.cs b/src/Avalonia.Controls/Calendar/CalendarExtensions.cs new file mode 100644 index 0000000000..485f2dfc95 --- /dev/null +++ b/src/Avalonia.Controls/Calendar/CalendarExtensions.cs @@ -0,0 +1,78 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using System; +using System.Collections.Generic; +using Avalonia.Input; +using System.Diagnostics; + +namespace Avalonia.Controls.Primitives +{ + internal static class CalendarExtensions + { + + private static Dictionary> _suspendedHandlers = new Dictionary>(); + + public static bool IsHandlerSuspended(this IAvaloniaObject obj, AvaloniaProperty dependencyProperty) + { + if (_suspendedHandlers.ContainsKey(obj)) + { + return _suspendedHandlers[obj].ContainsKey(dependencyProperty); + } + else + { + return false; + } + } + private static void SuspendHandler(this IAvaloniaObject obj, AvaloniaProperty dependencyProperty, bool suspend) + { + if (_suspendedHandlers.ContainsKey(obj)) + { + Dictionary suspensions = _suspendedHandlers[obj]; + + if (suspend) + { + Debug.Assert(!suspensions.ContainsKey(dependencyProperty), "Suspensions should not contain the property!"); + + // true = dummy value + suspensions[dependencyProperty] = true; + } + else + { + Debug.Assert(suspensions.ContainsKey(dependencyProperty), "Suspensions should contain the property!"); + suspensions.Remove(dependencyProperty); + if (suspensions.Count == 0) + { + _suspendedHandlers.Remove(obj); + } + } + } + else + { + Debug.Assert(suspend, "suspend should be true!"); + _suspendedHandlers[obj] = new Dictionary(); + _suspendedHandlers[obj][dependencyProperty] = true; + } + } + public static void SetValueNoCallback(this IAvaloniaObject obj, AvaloniaProperty property, T value) + { + obj.SuspendHandler(property, true); + try + { + obj.SetValue(property, value); + } + finally + { + obj.SuspendHandler(property, false); + } + } + + public static void GetMetaKeyState(InputModifiers modifiers, out bool ctrl, out bool shift) + { + ctrl = (modifiers & InputModifiers.Control) == InputModifiers.Control; + shift = (modifiers & InputModifiers.Shift) == InputModifiers.Shift; + } + } +} diff --git a/src/Avalonia.Controls/Calendar/CalendarItem.cs b/src/Avalonia.Controls/Calendar/CalendarItem.cs new file mode 100644 index 0000000000..fc358ed06e --- /dev/null +++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs @@ -0,0 +1,1263 @@ +// (c) Copyright Microsoft Corporation. +// This source is subject to the Microsoft Public License (Ms-PL). +// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details. +// All other rights reserved. + +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using System; +using System.Diagnostics; +using System.Globalization; + +namespace Avalonia.Controls.Primitives +{ + /// + /// Represents the currently displayed month or year on a + /// . + /// + public sealed class CalendarItem : TemplatedControl + { + /// + /// The number of days per week. + /// + private const int NumberOfDaysPerWeek = 7; + + private const string PART_ElementHeaderButton = "HeaderButton"; + private const string PART_ElementPreviousButton = "PreviousButton"; + private const string PART_ElementNextButton = "NextButton"; + private const string PART_ElementMonthView = "MonthView"; + private const string PART_ElementYearView = "YearView"; + + private Button _headerButton; + private Button _nextButton; + private Button _previousButton; + private Grid _monthView; + private Grid _yearView; + private ITemplate _dayTitleTemplate; + private CalendarButton _lastCalendarButton; + private CalendarDayButton _lastCalendarDayButton; + + private DateTime _currentMonth; + private bool _isMouseLeftButtonDown = false; + private bool _isMouseLeftButtonDownYearView = false; + private bool _isControlPressed = false; + + private System.Globalization.Calendar _calendar = new System.Globalization.GregorianCalendar(); + + private PointerPressedEventArgs _downEventArg; + private PointerPressedEventArgs _downEventArgYearView; + + internal Calendar Owner { get; set; } + internal CalendarDayButton CurrentButton { get; set; } + + public static StyledProperty HeaderBackgroundProperty = Calendar.HeaderBackgroundProperty.AddOwner(); + public IBrush HeaderBackground + { + get { return GetValue(HeaderBackgroundProperty); } + set { SetValue(HeaderBackgroundProperty, value); } + } + public static readonly DirectProperty> DayTitleTemplateProperty = + AvaloniaProperty.RegisterDirect>( + nameof(DayTitleTemplate), + o => o.DayTitleTemplate, + (o,v) => o.DayTitleTemplate = v, + defaultBindingMode: BindingMode.OneTime); + public ITemplate DayTitleTemplate + { + get { return _dayTitleTemplate; } + set { SetAndRaise(DayTitleTemplateProperty, ref _dayTitleTemplate, value); } + } + + /// + /// Gets the button that allows switching between month mode, year mode, + /// and decade mode. + /// + internal Button HeaderButton + { + get { return _headerButton; } + private set + { + if (_headerButton != null) + _headerButton.Click -= HeaderButton_Click; + + _headerButton = value; + + if (_headerButton != null) + { + _headerButton.Click += HeaderButton_Click; + _headerButton.Focusable = false; + } + } + } + /// + /// Gets the button that displays the next page of the calendar when it + /// is clicked. + /// + internal Button NextButton + { + get { return _nextButton; } + private set + { + if (_nextButton != null) + _nextButton.Click -= NextButton_Click; + + _nextButton = value; + + if (_nextButton != null) + { + // If the user does not provide a Content value in template, + // we provide a helper text that can be used in + // Accessibility this text is not shown on the UI, just used + // for Accessibility purposes + if (_nextButton.Content == null) + { + _nextButton.Content = "next button"; + } + + _nextButton.IsVisible = true; + _nextButton.Click += NextButton_Click; + _nextButton.Focusable = false; + } + } + } + /// + /// Gets the button that displays the previous page of the calendar when + /// it is clicked. + /// + internal Button PreviousButton + { + get { return _previousButton; } + private set + { + if (_previousButton != null) + _previousButton.Click -= PreviousButton_Click; + + _previousButton = value; + + if (_previousButton != null) + { + // If the user does not provide a Content value in template, + // we provide a helper text that can be used in + // Accessibility this text is not shown on the UI, just used + // for Accessibility purposes + if (_previousButton.Content == null) + { + _previousButton.Content = "previous button"; + } + + _previousButton.IsVisible = true; + _previousButton.Click += PreviousButton_Click; + _previousButton.Focusable = false; + } + } + } + + /// + /// Gets the Grid that hosts the content when in month mode. + /// + internal Grid MonthView + { + get { return _monthView; } + private set + { + if (_monthView != null) + _monthView.PointerLeave -= MonthView_MouseLeave; + + _monthView = value; + + if (_monthView != null) + _monthView.PointerLeave += MonthView_MouseLeave; + } + } + /// + /// Gets the Grid that hosts the content when in year or decade mode. + /// + internal Grid YearView + { + get { return _yearView; } + private set + { + if (_yearView != null) + _yearView.PointerLeave -= YearView_MouseLeave; + + _yearView = value; + + if (_yearView != null) + _yearView.PointerLeave += YearView_MouseLeave; + } + } + + private void PopulateGrids() + { + if (MonthView != null) + { + for (int i = 0; i < Calendar.RowsPerMonth; i++) + { + if (_dayTitleTemplate != null) + { + var cell = _dayTitleTemplate.Build(); + cell.SetValue(Grid.RowProperty, 0); + cell.SetValue(Grid.ColumnProperty, i); + MonthView.Children.Add(cell); + } + } + + for (int i = 1; i < Calendar.RowsPerMonth; i++) + { + for (int j = 0; j < Calendar.ColumnsPerMonth; j++) + { + CalendarDayButton cell = new CalendarDayButton(); + + if (Owner != null) + { + cell.Owner = Owner; + } + cell.SetValue(Grid.RowProperty, i); + cell.SetValue(Grid.ColumnProperty, j); + cell.CalendarDayButtonMouseDown += Cell_MouseLeftButtonDown; + cell.CalendarDayButtonMouseUp += Cell_MouseLeftButtonUp; + cell.PointerEnter += Cell_MouseEnter; + cell.PointerLeave += Cell_MouseLeave; + cell.Click += Cell_Click; + MonthView.Children.Add(cell); + } + } + } + + if (YearView != null) + { + CalendarButton month; + for (int i = 0; i < Calendar.RowsPerYear; i++) + { + for (int j = 0; j < Calendar.ColumnsPerYear; j++) + { + month = new CalendarButton(); + + if (Owner != null) + { + month.Owner = Owner; + } + month.SetValue(Grid.RowProperty, i); + month.SetValue(Grid.ColumnProperty, j); + month.CalendarLeftMouseButtonDown += Month_CalendarButtonMouseDown; + month.CalendarLeftMouseButtonUp += Month_CalendarButtonMouseUp; + month.PointerEnter += Month_MouseEnter; + month.PointerLeave += Month_MouseLeave; + YearView.Children.Add(month); + } + } + } + } + + /// + /// Builds the visual tree for the + /// + /// when a new template is applied. + /// + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + base.OnTemplateApplied(e); + + HeaderButton = e.NameScope.Find