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..8c79e5dce5
--- /dev/null
+++ b/src/Avalonia.Controls/Calendar/Calendar.cs
@@ -0,0 +1,2132 @@
+// (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;
+ private bool _displayDateIsChanging = false;
+
+ 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),
+ validate: ValidateDisplayMode);
+ ///
+ /// 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 (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));
+ }
+ private static CalendarMode ValidateDisplayMode(Calendar o, CalendarMode mode)
+ {
+ if(IsValidDisplayMode(mode))
+ {
+ return mode;
+ }
+ else
+ {
+ throw new ArgumentOutOfRangeException(nameof(mode), "Invalid DisplayMode");
+ }
+ }
+ 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))
+ {
+ _displayDateIsChanging = true;
+ SelectedDate = null;
+ _displayDateIsChanging = false;
+ 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 (!_displayDateIsChanging)
+ {
+ 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 (!_displayDateIsChanging)
+ {
+ 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 (!_displayDateIsChanging)
+ {
+ 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
+ {
+ cal._displayDateIsChanging = true;
+ if (DateTime.Compare(value.Value, cal.DisplayDateRangeStart) < 0)
+ {
+ cal.DisplayDateStart = value;
+ }
+ else if (DateTime.Compare(value.Value, cal.DisplayDateRangeEnd) > 0)
+ {
+ cal.DisplayDateEnd = value;
+ }
+ cal._displayDateIsChanging = false;
+
+ 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..0d48418683
--- /dev/null
+++ b/src/Avalonia.Controls/Calendar/CalendarBlackoutDatesCollection.cs
@@ -0,0 +1,215 @@
+// (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.Threading;
+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;
+
+ ///
+ /// Initializes a new instance of the
+ ///
+ /// class.
+ ///
+ ///
+ /// The whose dates
+ /// this object represents.
+ ///
+ public CalendarBlackoutDatesCollection(Calendar owner)
+ {
+ _owner = owner ?? throw new ArgumentNullException(nameof(owner));
+ }
+
+ ///
+ /// 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()
+ {
+ Dispatcher.UIThread.VerifyAccess();
+ }
+ }
+}
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..4fda02bff3
--- /dev/null
+++ b/src/Avalonia.Controls/Calendar/CalendarExtensions.cs
@@ -0,0 +1,21 @@
+// (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
+ {
+ 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..3432fa549d
--- /dev/null
+++ b/src/Avalonia.Controls/Calendar/CalendarItem.cs
@@ -0,0 +1,1264 @@
+// (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.DataContext = string.Empty;
+ 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml
index 48f530cca8..ae00f0757a 100644
--- a/src/Avalonia.Themes.Default/DefaultTheme.xaml
+++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml
@@ -36,4 +36,8 @@
+
+
+
+
diff --git a/tests/Avalonia.Controls.UnitTests/CalendarTests.cs b/tests/Avalonia.Controls.UnitTests/CalendarTests.cs
new file mode 100644
index 0000000000..4c45b1b230
--- /dev/null
+++ b/tests/Avalonia.Controls.UnitTests/CalendarTests.cs
@@ -0,0 +1,277 @@
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Xunit;
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Controls.UnitTests
+{
+ public class CalendarTests
+ {
+ private static bool CompareDates(DateTime first, DateTime second)
+ {
+ return first.Year == second.Year &&
+ first.Month == second.Month &&
+ first.Day == second.Day;
+ }
+
+ [Fact]
+ public void SelectedDatesChanged_Should_Fire_When_SelectedDate_Set()
+ {
+ bool handled = false;
+ Calendar calendar = new Calendar();
+ calendar.SelectionMode = CalendarSelectionMode.SingleDate;
+ calendar.SelectedDatesChanged += new EventHandler(delegate
+ {
+ handled = true;
+ });
+ DateTime value = new DateTime(2000, 10, 10);
+ calendar.SelectedDate = value;
+ Assert.True(handled);
+ }
+
+ [Fact]
+ public void DisplayDateChanged_Should_Fire_When_DisplayDate_Set()
+ {
+ bool handled = false;
+ Calendar calendar = new Calendar();
+ calendar.SelectionMode = CalendarSelectionMode.SingleDate;
+ calendar.DisplayDateChanged += new EventHandler(delegate
+ {
+ handled = true;
+ });
+ DateTime value = new DateTime(2000, 10, 10);
+ calendar.DisplayDate = value;
+ Assert.True(handled);
+ }
+
+ [Fact]
+ public void Setting_Selected_Date_To_Blackout_Date_Should_Throw()
+ {
+ Calendar calendar = new Calendar();
+ calendar.BlackoutDates.AddDatesInPast();
+
+ Assert.ThrowsAny(
+ () => calendar.SelectedDate = DateTime.Today.AddDays(-1));
+ }
+
+ [Fact]
+ public void Setting_Selected_Date_To_Blackout_Date_Should_Throw_Range()
+ {
+ Calendar calendar = new Calendar();
+ calendar.BlackoutDates.Add(new CalendarDateRange(DateTime.Today, DateTime.Today.AddDays(10)));
+
+ calendar.SelectedDate = DateTime.Today.AddDays(-1);
+ Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today.AddDays(-1)));
+ Assert.True(CompareDates(calendar.SelectedDate.Value, calendar.SelectedDates[0]));
+
+ calendar.SelectedDate = DateTime.Today.AddDays(11);
+ Assert.True(CompareDates(calendar.SelectedDate.Value, DateTime.Today.AddDays(11)));
+ Assert.True(CompareDates(calendar.SelectedDate.Value, calendar.SelectedDates[0]));
+
+ Assert.ThrowsAny(
+ () => calendar.SelectedDate = DateTime.Today.AddDays(5));
+ }
+
+ [Fact]
+ public void Adding_Blackout_Dates_Containing_Selected_Date_Should_Throw()
+ {
+ Calendar calendar = new Calendar();
+ calendar.SelectedDate = DateTime.Today.AddDays(5);
+
+ Assert.ThrowsAny(
+ () => calendar.BlackoutDates.Add(new CalendarDateRange(DateTime.Today, DateTime.Today.AddDays(10))));
+ }
+
+ [Fact]
+ public void DisplayDateStartEnd_Should_Constrain_Display_Date()
+ {
+ Calendar calendar = new Calendar();
+ calendar.SelectionMode = CalendarSelectionMode.SingleDate;
+ calendar.DisplayDateStart = new DateTime(2005, 12, 30);
+
+ DateTime value = new DateTime(2005, 12, 15);
+ calendar.DisplayDate = value;
+ Assert.True(CompareDates(calendar.DisplayDate, calendar.DisplayDateStart.Value));
+
+ value = new DateTime(2005, 12, 30);
+ calendar.DisplayDate = value;
+ Assert.True(CompareDates(calendar.DisplayDate, value));
+
+ value = DateTime.MaxValue;
+ calendar.DisplayDate = value;
+ Assert.True(CompareDates(calendar.DisplayDate, value));
+
+ calendar.DisplayDateEnd = new DateTime(2010, 12, 30);
+ Assert.True(CompareDates(calendar.DisplayDate, calendar.DisplayDateEnd.Value));
+ }
+
+ [Fact]
+ public void Setting_DisplayDateEnd_Should_Alter_DispalyDate_And_DisplayDateStart()
+ {
+ Calendar calendar = new Calendar();
+ DateTime value = new DateTime(2000, 1, 30);
+
+ calendar.DisplayDate = value;
+ calendar.DisplayDateEnd = value;
+ calendar.DisplayDateStart = value;
+ Assert.True(CompareDates(calendar.DisplayDateStart.Value, value));
+ Assert.True(CompareDates(calendar.DisplayDateEnd.Value, value));
+
+ value = value.AddMonths(2);
+ calendar.DisplayDateStart = value;
+ Assert.True(CompareDates(calendar.DisplayDateStart.Value, value));
+ Assert.True(CompareDates(calendar.DisplayDateEnd.Value, value));
+ Assert.True(CompareDates(calendar.DisplayDate, value));
+ }
+
+ [Fact]
+ public void Display_Date_Range_End_Will_Contain_SelectedDate()
+ {
+ Calendar calendar = new Calendar();
+ calendar.SelectionMode = CalendarSelectionMode.SingleDate;
+
+ calendar.SelectedDate = DateTime.MaxValue;
+ Assert.True(CompareDates((DateTime)calendar.SelectedDate, DateTime.MaxValue));
+
+ calendar.DisplayDateEnd = DateTime.MaxValue.AddDays(-1);
+ Assert.True(CompareDates((DateTime)calendar.DisplayDateEnd, DateTime.MaxValue));
+ }
+
+
+ ///
+ /// The days added to the SelectedDates collection.
+ ///
+ private IList