diff --git a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs index 2973e4ee..009eb9bb 100644 --- a/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarAppShellViewModel.cs @@ -99,6 +99,7 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, { Messenger.Send(new CalendarDisplayTypeChangedMessage(StatePersistenceService.CalendarDisplayType)); + UpdateDateNavigationHeaderItems(); // Change the calendar. DateClicked(new CalendarViewDayClickedEventArgs(GetDisplayTypeSwitchDate())); @@ -253,8 +254,6 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, break; case CalendarDisplayType.Month: break; - case CalendarDisplayType.Year: - break; default: break; } @@ -320,49 +319,88 @@ public partial class CalendarAppShellViewModel : CalendarBaseViewModel, /// private void UpdateDateNavigationHeaderItems() { - DateNavigationHeaderItems.Clear(); + var settings = PreferencesService.GetCurrentCalendarSettings(); + var cultureInfo = settings.CultureInfo ?? CultureInfo.CurrentUICulture; - // TODO: From settings - var testInfo = new CultureInfo("en-US"); + var visibleRange = HighlightedDateRange ?? new DateRange(DateTime.Today, DateTime.Today.AddDays(1)); + var headerText = GetHeaderText(visibleRange, cultureInfo); + + DateNavigationHeaderItems.ReplaceRange([headerText]); + SelectedDateNavigationHeaderIndex = DateNavigationHeaderItems.Count > 0 ? 0 : -1; + } + + private string GetHeaderText(DateRange visibleRange, CultureInfo cultureInfo) + { + var startDate = visibleRange.StartDate.Date; + var endDate = visibleRange.EndDate.Date > startDate ? visibleRange.EndDate.Date.AddDays(-1) : startDate; switch (StatePersistenceService.CalendarDisplayType) { case CalendarDisplayType.Day: + return startDate.ToString("MMMM d, dddd", cultureInfo); case CalendarDisplayType.Week: case CalendarDisplayType.WorkWeek: + if (startDate.Month == endDate.Month && startDate.Year == endDate.Year) + { + return $"{startDate.ToString("MMMM d", cultureInfo)} - {endDate.ToString("%d", cultureInfo)}"; + } + + return $"{startDate.ToString("MMMM d", cultureInfo)} - {endDate.ToString("MMMM d", cultureInfo)}"; case CalendarDisplayType.Month: - DateNavigationHeaderItems.ReplaceRange(testInfo.DateTimeFormat.MonthNames); - break; - case CalendarDisplayType.Year: - break; + return GetDominantMonthHeaderText(startDate, endDate, cultureInfo); default: - break; + return startDate.ToString("d", cultureInfo); } - - SetDateNavigationHeaderItems(); } - partial void OnHighlightedDateRangeChanged(DateRange value) => SetDateNavigationHeaderItems(); - - private void SetDateNavigationHeaderItems() + private static string GetDominantMonthHeaderText(DateTime startDate, DateTime endDate, CultureInfo cultureInfo) { - if (HighlightedDateRange == null) return; - - if (DateNavigationHeaderItems.Count == 0) + if (endDate < startDate) { - UpdateDateNavigationHeaderItems(); + endDate = startDate; } - // TODO: Year view - var monthIndex = HighlightedDateRange.GetMostVisibleMonthIndex(); + var monthDayCounts = new Dictionary<(int Year, int Month), int>(); - SelectedDateNavigationHeaderIndex = Math.Max(monthIndex - 1, -1); + for (var day = startDate; day <= endDate; day = day.AddDays(1)) + { + var key = (day.Year, day.Month); + + if (monthDayCounts.TryGetValue(key, out var count)) + { + monthDayCounts[key] = count + 1; + } + else + { + monthDayCounts[key] = 1; + } + } + + var dominantKey = (Year: startDate.Year, Month: startDate.Month); + var dominantCount = -1; + + foreach (var pair in monthDayCounts) + { + if (pair.Value > dominantCount) + { + dominantCount = pair.Value; + dominantKey = pair.Key; + } + } + + return new DateTime(dominantKey.Year, dominantKey.Month, 1).ToString("Y", cultureInfo); } + partial void OnHighlightedDateRangeChanged(DateRange value) => UpdateDateNavigationHeaderItems(); + public async void Receive(CalendarEnableStatusChangedMessage message) => await ExecuteUIThread(() => IsCalendarEnabled = message.IsEnabled); public void Receive(NavigateManageAccountsRequested message) => SelectedMenuItemIndex = 1; - public void Receive(CalendarDisplayTypeChangedMessage message) => OnPropertyChanged(nameof(IsVerticalCalendar)); + public void Receive(CalendarDisplayTypeChangedMessage message) + { + OnPropertyChanged(nameof(IsVerticalCalendar)); + UpdateDateNavigationHeaderItems(); + } } diff --git a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs index ed0c6164..d154ce49 100644 --- a/Wino.Calendar.ViewModels/CalendarPageViewModel.cs +++ b/Wino.Calendar.ViewModels/CalendarPageViewModel.cs @@ -232,7 +232,14 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, { base.OnNavigatedTo(mode, parameters); - if (mode == NavigationMode.Back) return; + if (mode == NavigationMode.Back) + { + // We unregister recipients on navigate-away, so mutations that happened while this page + // was not active (e.g. CalendarItemDeleted from details page) can be missed. + // Rehydrate currently visible ranges to guarantee UI and DB are consistent on return. + _ = RefreshVisibleRangesAsync(); + return; + } RefreshSettings(); @@ -682,6 +689,34 @@ public partial class CalendarPageViewModel : CalendarBaseViewModel, } } + private async Task RefreshVisibleRangesAsync() + { + try + { + await _calendarLoadingSemaphore.WaitAsync().ConfigureAwait(false); + + if (DayRanges == null || DayRanges.Count == 0) + return; + + RefreshSettings(); + + foreach (var dayRange in DayRanges) + { + await InitializeCalendarEventsForDayRangeAsync(dayRange).ConfigureAwait(false); + } + + FilterActiveCalendars(DayRanges); + } + catch (Exception ex) + { + Log.Error(ex, "Failed to refresh calendar ranges after navigation back."); + } + finally + { + _calendarLoadingSemaphore.Release(); + } + } + private async Task TryConsolidateItemsAsync() { // Check if trimming is necessary diff --git a/Wino.Calendar/Controls/CustomCalendarFlipView.cs b/Wino.Calendar/Controls/CustomCalendarFlipView.cs index 8d73220e..40b161a0 100644 --- a/Wino.Calendar/Controls/CustomCalendarFlipView.cs +++ b/Wino.Calendar/Controls/CustomCalendarFlipView.cs @@ -1,5 +1,7 @@ -using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation.Peers; using Windows.UI.Xaml.Controls; +using Wino.Core.Domain.Enums; namespace Wino.Calendar.Controls; @@ -8,35 +10,73 @@ namespace Wino.Calendar.Controls; /// public partial class CustomCalendarFlipView : FlipView { - private const string PART_PreviousButton = "PreviousButtonHorizontal"; - private const string PART_NextButton = "NextButtonHorizontal"; + private const string PART_PreviousButtonHorizontal = "PreviousButtonHorizontal"; + private const string PART_NextButtonHorizontal = "NextButtonHorizontal"; + private const string PART_PreviousButtonVertical = "PreviousButtonVertical"; + private const string PART_NextButtonVertical = "NextButtonVertical"; - private Button PreviousButton; - private Button NextButton; + public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register( + nameof(DisplayType), + typeof(CalendarDisplayType), + typeof(CustomCalendarFlipView), + new PropertyMetadata(CalendarDisplayType.Week)); + + public CalendarDisplayType DisplayType + { + get => (CalendarDisplayType)GetValue(DisplayTypeProperty); + set => SetValue(DisplayTypeProperty, value); + } + + private Button PreviousButtonHorizontal; + private Button NextButtonHorizontal; + private Button PreviousButtonVertical; + private Button NextButtonVertical; protected override void OnApplyTemplate() { base.OnApplyTemplate(); - PreviousButton = GetTemplateChild(PART_PreviousButton) as Button; - NextButton = GetTemplateChild(PART_NextButton) as Button; + PreviousButtonHorizontal = GetTemplateChild(PART_PreviousButtonHorizontal) as Button; + NextButtonHorizontal = GetTemplateChild(PART_NextButtonHorizontal) as Button; + PreviousButtonVertical = GetTemplateChild(PART_PreviousButtonVertical) as Button; + NextButtonVertical = GetTemplateChild(PART_NextButtonVertical) as Button; // Hide navigation buttons - PreviousButton.Opacity = NextButton.Opacity = 0; - PreviousButton.IsHitTestVisible = NextButton.IsHitTestVisible = false; + HideButton(PreviousButtonHorizontal); + HideButton(NextButtonHorizontal); + HideButton(PreviousButtonVertical); + HideButton(NextButtonVertical); + } - var t = FindName("ScrollingHost"); + private static void HideButton(Button button) + { + if (button == null) return; + + button.Opacity = 0; + button.IsHitTestVisible = false; } public void GoPreviousFlip() { - var backPeer = new ButtonAutomationPeer(PreviousButton); + var previousButton = DisplayType == CalendarDisplayType.Month + ? PreviousButtonVertical ?? PreviousButtonHorizontal + : PreviousButtonHorizontal ?? PreviousButtonVertical; + + if (previousButton == null) return; + + var backPeer = new ButtonAutomationPeer(previousButton); backPeer.Invoke(); } public void GoNextFlip() { - var nextPeer = new ButtonAutomationPeer(NextButton); + var nextButton = DisplayType == CalendarDisplayType.Month + ? NextButtonVertical ?? NextButtonHorizontal + : NextButtonHorizontal ?? NextButtonVertical; + + if (nextButton == null) return; + + var nextPeer = new ButtonAutomationPeer(nextButton); nextPeer.Invoke(); } } diff --git a/Wino.Calendar/Controls/DayColumnControl.cs b/Wino.Calendar/Controls/DayColumnControl.cs index a3f3cb78..7293886e 100644 --- a/Wino.Calendar/Controls/DayColumnControl.cs +++ b/Wino.Calendar/Controls/DayColumnControl.cs @@ -1,6 +1,9 @@ -using System; +using System; +using System.Collections.Specialized; +using System.Linq; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; +using Wino.Core.Domain.Collections; using Wino.Core.Domain.Models.Calendar; namespace Wino.Calendar.Controls; @@ -10,7 +13,6 @@ public partial class DayColumnControl : Control private const string PART_HeaderDateDayText = nameof(PART_HeaderDateDayText); private const string PART_IsTodayBorder = nameof(PART_IsTodayBorder); private const string PART_ColumnHeaderText = nameof(PART_ColumnHeaderText); - private const string PART_AllDayItemsControl = nameof(PART_AllDayItemsControl); private const string TodayState = nameof(TodayState); @@ -20,6 +22,7 @@ public partial class DayColumnControl : Control private TextBlock ColumnHeaderText; private Border IsTodayBorder; private ItemsControl AllDayItemsControl; + private CalendarEventCollection _boundEventsCollection; public CalendarDayModel DayModel { @@ -27,11 +30,16 @@ public partial class DayColumnControl : Control set { SetValue(DayModelProperty, value); } } - public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register(nameof(DayModel), typeof(CalendarDayModel), typeof(DayColumnControl), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged))); + public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register( + nameof(DayModel), + typeof(CalendarDayModel), + typeof(DayColumnControl), + new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged))); public DayColumnControl() { DefaultStyleKey = typeof(DayColumnControl); + Unloaded += OnUnloaded; } protected override void OnApplyTemplate() @@ -43,6 +51,7 @@ public partial class DayColumnControl : Control IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border; AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as ItemsControl; + RegisterEventsCollectionHandlers(); UpdateValues(); } @@ -50,15 +59,78 @@ public partial class DayColumnControl : Control { if (control is DayColumnControl columnControl) { + columnControl.RegisterEventsCollectionHandlers(); columnControl.UpdateValues(); } } + private void OnUnloaded(object sender, RoutedEventArgs e) + { + DeregisterEventsCollectionHandlers(); + } + + private bool IsMonthlyTemplate() => ColumnHeaderText == null; + + private void RegisterEventsCollectionHandlers() + { + var nextCollection = DayModel?.EventsCollection; + if (ReferenceEquals(_boundEventsCollection, nextCollection)) + return; + + DeregisterEventsCollectionHandlers(); + + _boundEventsCollection = nextCollection; + if (_boundEventsCollection == null) + return; + + ((INotifyCollectionChanged)_boundEventsCollection.AllDayEvents).CollectionChanged += EventsCollectionChanged; + ((INotifyCollectionChanged)_boundEventsCollection.RegularEvents).CollectionChanged += EventsCollectionChanged; + } + + private void DeregisterEventsCollectionHandlers() + { + if (_boundEventsCollection == null) + return; + + ((INotifyCollectionChanged)_boundEventsCollection.AllDayEvents).CollectionChanged -= EventsCollectionChanged; + ((INotifyCollectionChanged)_boundEventsCollection.RegularEvents).CollectionChanged -= EventsCollectionChanged; + _boundEventsCollection = null; + } + + private void EventsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateEventItemsSource(); + } + + private void UpdateEventItemsSource() + { + if (AllDayItemsControl == null || DayModel == null) return; + + if (IsMonthlyTemplate()) + { + // Month cells should show all events for the day, not only all-day/multi-day. + var monthlyItems = DayModel.EventsCollection.AllDayEvents + .Concat(DayModel.EventsCollection.RegularEvents) + .GroupBy(a => a.Id) + .Select(g => g.First()) + .OrderBy(a => a.StartDate) + .ToList(); + + AllDayItemsControl.ItemsSource = monthlyItems; + return; + } + + AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents; + } + private void UpdateValues() { - if (HeaderDateDayText == null || IsTodayBorder == null || DayModel == null) return; + if (DayModel == null) return; - HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString(); + if (HeaderDateDayText != null) + { + HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString(); + } // Monthly template does not use it. if (ColumnHeaderText != null) @@ -66,8 +138,9 @@ public partial class DayColumnControl : Control ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo); } - AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents; + UpdateEventItemsSource(); + if (IsTodayBorder == null) return; bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date; VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false); diff --git a/Wino.Calendar/Controls/WinoCalendarControl.cs b/Wino.Calendar/Controls/WinoCalendarControl.cs index 11a37109..3f613041 100644 --- a/Wino.Calendar/Controls/WinoCalendarControl.cs +++ b/Wino.Calendar/Controls/WinoCalendarControl.cs @@ -34,7 +34,7 @@ public partial class WinoCalendarControl : Control public static readonly DependencyProperty VerticalItemsPanelTemplateProperty = DependencyProperty.Register(nameof(VerticalItemsPanelTemplate), typeof(ItemsPanelTemplate), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated))); public static readonly DependencyProperty HorizontalItemsPanelTemplateProperty = DependencyProperty.Register(nameof(HorizontalItemsPanelTemplate), typeof(ItemsPanelTemplate), typeof(WinoCalendarControl), new PropertyMetadata(null, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated))); public static readonly DependencyProperty OrientationProperty = DependencyProperty.Register(nameof(Orientation), typeof(CalendarOrientation), typeof(WinoCalendarControl), new PropertyMetadata(CalendarOrientation.Horizontal, new PropertyChangedCallback(OnCalendarOrientationPropertiesUpdated))); - public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(CalendarDisplayType), typeof(WinoCalendarControl), new PropertyMetadata(CalendarDisplayType.Day)); + public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(CalendarDisplayType), typeof(WinoCalendarControl), new PropertyMetadata(CalendarDisplayType.Day, new PropertyChangedCallback(OnDisplayTypeChanged))); /// /// Gets or sets the day-week-month-year display type. @@ -171,6 +171,14 @@ public partial class WinoCalendarControl : Control } } + private static void OnDisplayTypeChanged(DependencyObject calendar, DependencyPropertyChangedEventArgs e) + { + if (calendar is WinoCalendarControl calendarControl) + { + calendarControl.ManageDisplayType(); + } + } + private void ManageCalendarOrientation() { if (InternalFlipView == null || HorizontalItemsPanelTemplate == null || VerticalItemsPanelTemplate == null) return; @@ -178,6 +186,13 @@ public partial class WinoCalendarControl : Control InternalFlipView.ItemsPanel = Orientation == CalendarOrientation.Horizontal ? HorizontalItemsPanelTemplate : VerticalItemsPanelTemplate; } + private void ManageDisplayType() + { + if (InternalFlipView == null) return; + + InternalFlipView.DisplayType = DisplayType; + } + private void ManageHighlightedDateRange() => SelectedFlipViewDayRange = InternalFlipView.SelectedItem as DayRangeRenderModel; @@ -232,6 +247,7 @@ public partial class WinoCalendarControl : Control UpdateIdleState(); ManageCalendarOrientation(); + ManageDisplayType(); } private void UpdateIdleState() diff --git a/Wino.Calendar/Controls/WinoCalendarTypeSelectorControl.cs b/Wino.Calendar/Controls/WinoCalendarTypeSelectorControl.cs index 899e32da..e15c4330 100644 --- a/Wino.Calendar/Controls/WinoCalendarTypeSelectorControl.cs +++ b/Wino.Calendar/Controls/WinoCalendarTypeSelectorControl.cs @@ -12,9 +12,12 @@ public partial class WinoCalendarTypeSelectorControl : Control private const string PART_DayToggle = nameof(PART_DayToggle); private const string PART_WeekToggle = nameof(PART_WeekToggle); private const string PART_MonthToggle = nameof(PART_MonthToggle); - private const string PART_YearToggle = nameof(PART_YearToggle); - public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register(nameof(SelectedType), typeof(CalendarDisplayType), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(CalendarDisplayType.Week)); + public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register( + nameof(SelectedType), + typeof(CalendarDisplayType), + typeof(WinoCalendarTypeSelectorControl), + new PropertyMetadata(CalendarDisplayType.Week, new PropertyChangedCallback(OnSelectedTypeChanged))); public static readonly DependencyProperty DisplayDayCountProperty = DependencyProperty.Register(nameof(DisplayDayCount), typeof(int), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(0)); public static readonly DependencyProperty TodayClickedCommandProperty = DependencyProperty.Register(nameof(TodayClickedCommand), typeof(ICommand), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(null)); @@ -40,7 +43,6 @@ public partial class WinoCalendarTypeSelectorControl : Control private AppBarToggleButton _dayToggle; private AppBarToggleButton _weekToggle; private AppBarToggleButton _monthToggle; - private AppBarToggleButton _yearToggle; public WinoCalendarTypeSelectorControl() { @@ -51,24 +53,23 @@ public partial class WinoCalendarTypeSelectorControl : Control { base.OnApplyTemplate(); + UnregisterHandlers(); + _todayButton = GetTemplateChild(PART_TodayButton) as AppBarButton; _dayToggle = GetTemplateChild(PART_DayToggle) as AppBarToggleButton; _weekToggle = GetTemplateChild(PART_WeekToggle) as AppBarToggleButton; _monthToggle = GetTemplateChild(PART_MonthToggle) as AppBarToggleButton; - _yearToggle = GetTemplateChild(PART_YearToggle) as AppBarToggleButton; Guard.IsNotNull(_todayButton, nameof(_todayButton)); Guard.IsNotNull(_dayToggle, nameof(_dayToggle)); Guard.IsNotNull(_weekToggle, nameof(_weekToggle)); Guard.IsNotNull(_monthToggle, nameof(_monthToggle)); - Guard.IsNotNull(_yearToggle, nameof(_yearToggle)); _todayButton.Click += TodayClicked; _dayToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Day); }; _weekToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Week); }; _monthToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Month); }; - _yearToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Year); }; UpdateToggleButtonStates(); } @@ -81,11 +82,26 @@ public partial class WinoCalendarTypeSelectorControl : Control UpdateToggleButtonStates(); } + private static void OnSelectedTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as WinoCalendarTypeSelectorControl; + control?.UpdateToggleButtonStates(); + } + + private void UnregisterHandlers() + { + if (_todayButton != null) + { + _todayButton.Click -= TodayClicked; + } + } + private void UpdateToggleButtonStates() { + if (_dayToggle == null || _weekToggle == null || _monthToggle == null) return; + _dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day; _weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week; _monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month; - _yearToggle.IsChecked = SelectedType == CalendarDisplayType.Year; } } diff --git a/Wino.Calendar/Selectors/WinoCalendarItemTemplateSelector.cs b/Wino.Calendar/Selectors/WinoCalendarItemTemplateSelector.cs index 0474a188..36287b67 100644 --- a/Wino.Calendar/Selectors/WinoCalendarItemTemplateSelector.cs +++ b/Wino.Calendar/Selectors/WinoCalendarItemTemplateSelector.cs @@ -22,8 +22,6 @@ public partial class WinoCalendarItemTemplateSelector : DataTemplateSelector return DayWeekWorkWeekTemplate; case CalendarDisplayType.Month: return MonthlyTemplate; - case CalendarDisplayType.Year: - break; default: break; } diff --git a/Wino.Calendar/Styles/WinoCalendarResources.xaml b/Wino.Calendar/Styles/WinoCalendarResources.xaml index c0469ff1..6902dae7 100644 --- a/Wino.Calendar/Styles/WinoCalendarResources.xaml +++ b/Wino.Calendar/Styles/WinoCalendarResources.xaml @@ -367,6 +367,9 @@ + + + diff --git a/Wino.Calendar/Styles/WinoCalendarTypeSelectorControl.xaml b/Wino.Calendar/Styles/WinoCalendarTypeSelectorControl.xaml index 51aff54a..a7c374b9 100644 --- a/Wino.Calendar/Styles/WinoCalendarTypeSelectorControl.xaml +++ b/Wino.Calendar/Styles/WinoCalendarTypeSelectorControl.xaml @@ -61,16 +61,6 @@ - - - - - - - diff --git a/Wino.Calendar/Views/AppShell.xaml b/Wino.Calendar/Views/AppShell.xaml index 81a4591d..19ca397c 100644 --- a/Wino.Calendar/Views/AppShell.xaml +++ b/Wino.Calendar/Views/AppShell.xaml @@ -1,4 +1,4 @@ - diff --git a/Wino.Calendar/Views/AppShell.xaml.cs b/Wino.Calendar/Views/AppShell.xaml.cs index bfe5fa0f..25b54119 100644 --- a/Wino.Calendar/Views/AppShell.xaml.cs +++ b/Wino.Calendar/Views/AppShell.xaml.cs @@ -20,13 +20,13 @@ public sealed partial class AppShell : AppShellAbstract, InitializeComponent(); Window.Current.SetTitleBar(DragArea); - ManageCalendarDisplayType(); + ManageCalendarDisplayType(ViewModel.StatePersistenceService.CalendarDisplayType); } - private void ManageCalendarDisplayType() + private void ManageCalendarDisplayType(Core.Domain.Enums.CalendarDisplayType displayType) { // Go to different states based on the display type. - if (ViewModel.IsVerticalCalendar) + if (displayType == Core.Domain.Enums.CalendarDisplayType.Month) { VisualStateManager.GoToState(this, STATE_VerticalCalendar, false); } @@ -42,7 +42,7 @@ public sealed partial class AppShell : AppShellAbstract, public void Receive(CalendarDisplayTypeChangedMessage message) { - ManageCalendarDisplayType(); + ManageCalendarDisplayType(message.NewDisplayType); } private void ShellFrameContentNavigated(object sender, Windows.UI.Xaml.Navigation.NavigationEventArgs e) diff --git a/Wino.Core.Domain/Enums/CalendarDisplayType.cs b/Wino.Core.Domain/Enums/CalendarDisplayType.cs index 5582899b..afb838d2 100644 --- a/Wino.Core.Domain/Enums/CalendarDisplayType.cs +++ b/Wino.Core.Domain/Enums/CalendarDisplayType.cs @@ -5,6 +5,5 @@ public enum CalendarDisplayType Day, Week, WorkWeek, - Month, - Year + Month } diff --git a/Wino.Mail.WinUI/Controls/Calendar/CustomCalendarFlipView.cs b/Wino.Mail.WinUI/Controls/Calendar/CustomCalendarFlipView.cs index b564e833..36a291a3 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/CustomCalendarFlipView.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/CustomCalendarFlipView.cs @@ -1,7 +1,8 @@ -using System.Linq; +using System.Linq; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Automation.Peers; using Microsoft.UI.Xaml.Controls; +using Wino.Core.Domain.Enums; using Wino.Mail.WinUI.Controls.CalendarFlipView; namespace Wino.Calendar.Controls; @@ -11,27 +12,56 @@ namespace Wino.Calendar.Controls; /// public partial class CustomCalendarFlipView : FlipView { - private const string PART_PreviousButton = "PreviousButtonHorizontal"; - private const string PART_NextButton = "NextButtonHorizontal"; + private const string PART_PreviousButtonHorizontal = "PreviousButtonHorizontal"; + private const string PART_NextButtonHorizontal = "NextButtonHorizontal"; + private const string PART_PreviousButtonVertical = "PreviousButtonVertical"; + private const string PART_NextButtonVertical = "NextButtonVertical"; - private Button? PreviousButton; - private Button? NextButton; + public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register( + nameof(DisplayType), + typeof(CalendarDisplayType), + typeof(CustomCalendarFlipView), + new PropertyMetadata(CalendarDisplayType.Week)); + + public CalendarDisplayType DisplayType + { + get => (CalendarDisplayType)GetValue(DisplayTypeProperty); + set => SetValue(DisplayTypeProperty, value); + } + + private Button? PreviousButtonHorizontal; + private Button? NextButtonHorizontal; + private Button? PreviousButtonVertical; + private Button? NextButtonVertical; protected override void OnApplyTemplate() { base.OnApplyTemplate(); - PreviousButton = (Button)GetTemplateChild(PART_PreviousButton); - NextButton = (Button)GetTemplateChild(PART_NextButton); + PreviousButtonHorizontal = GetTemplateChild(PART_PreviousButtonHorizontal) as Button; + NextButtonHorizontal = GetTemplateChild(PART_NextButtonHorizontal) as Button; + PreviousButtonVertical = GetTemplateChild(PART_PreviousButtonVertical) as Button; + NextButtonVertical = GetTemplateChild(PART_NextButtonVertical) as Button; // Hide navigation buttons - PreviousButton.Opacity = NextButton.Opacity = 0; - PreviousButton.IsHitTestVisible = NextButton.IsHitTestVisible = false; + HideButton(PreviousButtonHorizontal); + HideButton(NextButtonHorizontal); + HideButton(PreviousButtonVertical); + HideButton(NextButtonVertical); - this.SelectionChanged += FlipViewSelectionChanged; + SelectionChanged += FlipViewSelectionChanged; } - private void FlipViewSelectionChanged(object sender, SelectionChangedEventArgs e) => OnSelectedItemChanged(e.RemovedItems.FirstOrDefault(), e.AddedItems.FirstOrDefault()); + private static void HideButton(Button? button) + { + if (button == null) return; + + button.Opacity = 0; + button.IsHitTestVisible = false; + } + + private void FlipViewSelectionChanged(object sender, SelectionChangedEventArgs e) + => OnSelectedItemChanged(e.RemovedItems.FirstOrDefault(), e.AddedItems.FirstOrDefault()); protected virtual void OnSelectedItemChanged(object oldValue, object newValue) { } @@ -47,13 +77,25 @@ public partial class CustomCalendarFlipView : FlipView public void GoPreviousFlip() { - var backPeer = new ButtonAutomationPeer(PreviousButton); + var previousButton = DisplayType == CalendarDisplayType.Month + ? PreviousButtonVertical ?? PreviousButtonHorizontal + : PreviousButtonHorizontal ?? PreviousButtonVertical; + + if (previousButton == null) return; + + var backPeer = new ButtonAutomationPeer(previousButton); backPeer.Invoke(); } public void GoNextFlip() { - var nextPeer = new ButtonAutomationPeer(NextButton); + var nextButton = DisplayType == CalendarDisplayType.Month + ? NextButtonVertical ?? NextButtonHorizontal + : NextButtonHorizontal ?? NextButtonVertical; + + if (nextButton == null) return; + + var nextPeer = new ButtonAutomationPeer(nextButton); nextPeer.Invoke(); } } diff --git a/Wino.Mail.WinUI/Controls/Calendar/DayColumnControl.cs b/Wino.Mail.WinUI/Controls/Calendar/DayColumnControl.cs index 50ccc128..2f0838e1 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/DayColumnControl.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/DayColumnControl.cs @@ -1,6 +1,9 @@ -using System; +using System; +using System.Collections.Specialized; +using System.Linq; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Wino.Core.Domain.Collections; using Wino.Core.Domain.Models.Calendar; namespace Wino.Calendar.Controls; @@ -10,7 +13,6 @@ public partial class DayColumnControl : Control private const string PART_HeaderDateDayText = nameof(PART_HeaderDateDayText); private const string PART_IsTodayBorder = nameof(PART_IsTodayBorder); private const string PART_ColumnHeaderText = nameof(PART_ColumnHeaderText); - private const string PART_AllDayItemsControl = nameof(PART_AllDayItemsControl); private const string TodayState = nameof(TodayState); @@ -20,6 +22,7 @@ public partial class DayColumnControl : Control private TextBlock ColumnHeaderText; private Border IsTodayBorder; private ItemsControl AllDayItemsControl; + private CalendarEventCollection _boundEventsCollection; public CalendarDayModel DayModel { @@ -27,11 +30,16 @@ public partial class DayColumnControl : Control set { SetValue(DayModelProperty, value); } } - public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register(nameof(DayModel), typeof(CalendarDayModel), typeof(DayColumnControl), new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged))); + public static readonly DependencyProperty DayModelProperty = DependencyProperty.Register( + nameof(DayModel), + typeof(CalendarDayModel), + typeof(DayColumnControl), + new PropertyMetadata(null, new PropertyChangedCallback(OnRenderingPropertiesChanged))); public DayColumnControl() { DefaultStyleKey = typeof(DayColumnControl); + Unloaded += OnUnloaded; } protected override void OnApplyTemplate() @@ -43,6 +51,7 @@ public partial class DayColumnControl : Control IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border; AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as ItemsControl; + RegisterEventsCollectionHandlers(); UpdateValues(); } @@ -50,15 +59,78 @@ public partial class DayColumnControl : Control { if (control is DayColumnControl columnControl) { + columnControl.RegisterEventsCollectionHandlers(); columnControl.UpdateValues(); } } + private void OnUnloaded(object sender, RoutedEventArgs e) + { + DeregisterEventsCollectionHandlers(); + } + + private bool IsMonthlyTemplate() => ColumnHeaderText == null; + + private void RegisterEventsCollectionHandlers() + { + var nextCollection = DayModel?.EventsCollection; + if (ReferenceEquals(_boundEventsCollection, nextCollection)) + return; + + DeregisterEventsCollectionHandlers(); + + _boundEventsCollection = nextCollection; + if (_boundEventsCollection == null) + return; + + ((INotifyCollectionChanged)_boundEventsCollection.AllDayEvents).CollectionChanged += EventsCollectionChanged; + ((INotifyCollectionChanged)_boundEventsCollection.RegularEvents).CollectionChanged += EventsCollectionChanged; + } + + private void DeregisterEventsCollectionHandlers() + { + if (_boundEventsCollection == null) + return; + + ((INotifyCollectionChanged)_boundEventsCollection.AllDayEvents).CollectionChanged -= EventsCollectionChanged; + ((INotifyCollectionChanged)_boundEventsCollection.RegularEvents).CollectionChanged -= EventsCollectionChanged; + _boundEventsCollection = null; + } + + private void EventsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) + { + UpdateEventItemsSource(); + } + + private void UpdateEventItemsSource() + { + if (AllDayItemsControl == null || DayModel == null) return; + + if (IsMonthlyTemplate()) + { + // Month cells should show all events for the day, not only all-day/multi-day. + var monthlyItems = DayModel.EventsCollection.AllDayEvents + .Concat(DayModel.EventsCollection.RegularEvents) + .GroupBy(a => a.Id) + .Select(g => g.First()) + .OrderBy(a => a.StartDate) + .ToList(); + + AllDayItemsControl.ItemsSource = monthlyItems; + return; + } + + AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents; + } + private void UpdateValues() { - if (HeaderDateDayText == null || IsTodayBorder == null || DayModel == null) return; + if (DayModel == null) return; - HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString(); + if (HeaderDateDayText != null) + { + HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString(); + } // Monthly template does not use it. if (ColumnHeaderText != null) @@ -66,8 +138,9 @@ public partial class DayColumnControl : Control ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo); } - AllDayItemsControl.ItemsSource = DayModel.EventsCollection.AllDayEvents; + UpdateEventItemsSource(); + if (IsTodayBorder == null) return; bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date; VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false); diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarControl.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarControl.cs index 192c0f47..93b02616 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarControl.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarControl.cs @@ -87,6 +87,9 @@ public partial class WinoCalendarControl : Control partial void OnOrientationChanged(CalendarOrientation newValue) => ManageCalendarOrientation(); + partial void OnDisplayTypeChanged(CalendarDisplayType newValue) + => ManageDisplayType(); + partial void OnIsFlipIdleChanged(bool newValue) => UpdateIdleState(); @@ -131,6 +134,13 @@ public partial class WinoCalendarControl : Control InternalFlipView.ItemsPanel = Orientation == CalendarOrientation.Horizontal ? HorizontalItemsPanelTemplate : VerticalItemsPanelTemplate; } + private void ManageDisplayType() + { + if (InternalFlipView == null) return; + + InternalFlipView.DisplayType = DisplayType; + } + private void ManageHighlightedDateRange() => SelectedFlipViewDayRange = InternalFlipView.SelectedItem as DayRangeRenderModel; @@ -185,6 +195,7 @@ public partial class WinoCalendarControl : Control UpdateIdleState(); ManageCalendarOrientation(); + ManageDisplayType(); } private void UpdateIdleState() diff --git a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarTypeSelectorControl.cs b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarTypeSelectorControl.cs index e2fbfa82..af396931 100644 --- a/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarTypeSelectorControl.cs +++ b/Wino.Mail.WinUI/Controls/Calendar/WinoCalendarTypeSelectorControl.cs @@ -12,9 +12,12 @@ public partial class WinoCalendarTypeSelectorControl : Control private const string PART_DayToggle = nameof(PART_DayToggle); private const string PART_WeekToggle = nameof(PART_WeekToggle); private const string PART_MonthToggle = nameof(PART_MonthToggle); - private const string PART_YearToggle = nameof(PART_YearToggle); - public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register(nameof(SelectedType), typeof(CalendarDisplayType), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(CalendarDisplayType.Week)); + public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register( + nameof(SelectedType), + typeof(CalendarDisplayType), + typeof(WinoCalendarTypeSelectorControl), + new PropertyMetadata(CalendarDisplayType.Week, OnSelectedTypeChanged)); public static readonly DependencyProperty DisplayDayCountProperty = DependencyProperty.Register(nameof(DisplayDayCount), typeof(int), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(0)); public static readonly DependencyProperty TodayClickedCommandProperty = DependencyProperty.Register(nameof(TodayClickedCommand), typeof(ICommand), typeof(WinoCalendarTypeSelectorControl), new PropertyMetadata(null)); @@ -40,7 +43,6 @@ public partial class WinoCalendarTypeSelectorControl : Control private AppBarToggleButton _dayToggle; private AppBarToggleButton _weekToggle; private AppBarToggleButton _monthToggle; - private AppBarToggleButton _yearToggle; public WinoCalendarTypeSelectorControl() { @@ -51,24 +53,23 @@ public partial class WinoCalendarTypeSelectorControl : Control { base.OnApplyTemplate(); + UnregisterHandlers(); + _todayButton = GetTemplateChild(PART_TodayButton) as AppBarButton; _dayToggle = GetTemplateChild(PART_DayToggle) as AppBarToggleButton; _weekToggle = GetTemplateChild(PART_WeekToggle) as AppBarToggleButton; _monthToggle = GetTemplateChild(PART_MonthToggle) as AppBarToggleButton; - _yearToggle = GetTemplateChild(PART_YearToggle) as AppBarToggleButton; Guard.IsNotNull(_todayButton, nameof(_todayButton)); Guard.IsNotNull(_dayToggle, nameof(_dayToggle)); Guard.IsNotNull(_weekToggle, nameof(_weekToggle)); Guard.IsNotNull(_monthToggle, nameof(_monthToggle)); - Guard.IsNotNull(_yearToggle, nameof(_yearToggle)); _todayButton.Click += TodayClicked; _dayToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Day); }; _weekToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Week); }; _monthToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Month); }; - _yearToggle.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Year); }; UpdateToggleButtonStates(); } @@ -81,11 +82,29 @@ public partial class WinoCalendarTypeSelectorControl : Control UpdateToggleButtonStates(); } + private static void OnSelectedTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = d as WinoCalendarTypeSelectorControl; + control?.UpdateToggleButtonStates(); + } + + private void UnregisterHandlers() + { + if (_todayButton != null) + { + _todayButton.Click -= TodayClicked; + } + } + private void UpdateToggleButtonStates() { + if (_dayToggle == null || _weekToggle == null || _monthToggle == null) + { + return; + } + _dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day; _weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week; _monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month; - _yearToggle.IsChecked = SelectedType == CalendarDisplayType.Year; } } diff --git a/Wino.Mail.WinUI/Selectors/WinoCalendarItemTemplateSelector.cs b/Wino.Mail.WinUI/Selectors/WinoCalendarItemTemplateSelector.cs index 2d3ec477..84f0d336 100644 --- a/Wino.Mail.WinUI/Selectors/WinoCalendarItemTemplateSelector.cs +++ b/Wino.Mail.WinUI/Selectors/WinoCalendarItemTemplateSelector.cs @@ -22,8 +22,6 @@ public partial class WinoCalendarItemTemplateSelector : DataTemplateSelector return DayWeekWorkWeekTemplate; case CalendarDisplayType.Month: return MonthlyTemplate; - case CalendarDisplayType.Year: - break; default: break; } diff --git a/Wino.Mail.WinUI/Services/StatePersistenceService.cs b/Wino.Mail.WinUI/Services/StatePersistenceService.cs index 4038d7fd..155d1386 100644 --- a/Wino.Mail.WinUI/Services/StatePersistenceService.cs +++ b/Wino.Mail.WinUI/Services/StatePersistenceService.cs @@ -22,7 +22,7 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic _openPaneLength = _configurationService.Get(OpenPaneLengthKey, 320d); _mailListPaneLength = _configurationService.Get(MailListPaneLengthKey, 420d); - _calendarDisplayType = _configurationService.Get(nameof(CalendarDisplayType), CalendarDisplayType.Week); + _calendarDisplayType = EnsureValidCalendarDisplayType(_configurationService.Get(nameof(CalendarDisplayType), CalendarDisplayType.Week)); _dayDisplayCount = _configurationService.Get(nameof(DayDisplayCount), 1); PropertyChanged += ServicePropertyChanged; @@ -176,9 +176,11 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic get => _calendarDisplayType; set { - if (SetProperty(ref _calendarDisplayType, value)) + var validValue = EnsureValidCalendarDisplayType(value); + + if (SetProperty(ref _calendarDisplayType, validValue)) { - _configurationService.Set(nameof(CalendarDisplayType), value); + _configurationService.Set(nameof(CalendarDisplayType), validValue); } } } @@ -197,4 +199,11 @@ public class StatePersistenceService : ObservableObject, IStatePersistanceServic } private void UpdateAppCoreWindowTitle() => WinoApplication.MainWindow.Title = CoreWindowTitle; + + private static CalendarDisplayType EnsureValidCalendarDisplayType(CalendarDisplayType displayType) + { + return Enum.IsDefined(typeof(CalendarDisplayType), displayType) + ? displayType + : CalendarDisplayType.Week; + } } diff --git a/Wino.Mail.WinUI/Styles/WinoCalendarResources.xaml b/Wino.Mail.WinUI/Styles/WinoCalendarResources.xaml index 242934c0..630d259d 100644 --- a/Wino.Mail.WinUI/Styles/WinoCalendarResources.xaml +++ b/Wino.Mail.WinUI/Styles/WinoCalendarResources.xaml @@ -390,6 +390,9 @@ + + + diff --git a/Wino.Mail.WinUI/Styles/WinoCalendarTypeSelectorControl.xaml b/Wino.Mail.WinUI/Styles/WinoCalendarTypeSelectorControl.xaml index 48fb8cb6..f5af97aa 100644 --- a/Wino.Mail.WinUI/Styles/WinoCalendarTypeSelectorControl.xaml +++ b/Wino.Mail.WinUI/Styles/WinoCalendarTypeSelectorControl.xaml @@ -61,16 +61,6 @@ - - - - - - - diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml index d0e03b7e..629cfc53 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml @@ -1,4 +1,4 @@ - diff --git a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml.cs b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml.cs index b50f0580..158ecc47 100644 --- a/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml.cs +++ b/Wino.Mail.WinUI/Views/Calendar/CalendarAppShell.xaml.cs @@ -19,13 +19,13 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract, InitializeComponent(); // Window.Current.SetTitleBar(DragArea); - ManageCalendarDisplayType(); + ManageCalendarDisplayType(ViewModel.StatePersistenceService.CalendarDisplayType); } - private void ManageCalendarDisplayType() + private void ManageCalendarDisplayType(Core.Domain.Enums.CalendarDisplayType displayType) { // Go to different states based on the display type. - if (ViewModel.IsVerticalCalendar) + if (displayType == Core.Domain.Enums.CalendarDisplayType.Month) { VisualStateManager.GoToState(this, STATE_VerticalCalendar, false); } @@ -41,7 +41,7 @@ public sealed partial class CalendarAppShell : CalendarAppShellAbstract, public void Receive(CalendarDisplayTypeChangedMessage message) { - ManageCalendarDisplayType(); + ManageCalendarDisplayType(message.NewDisplayType); } //private void ShellFrameContentNavigated(object sender, Microsoft.UI.Xaml.Navigation.NavigationEventArgs e)