Calendar rendering implementation.
This commit is contained in:
@@ -15,6 +15,9 @@ Wino Mail is a native Windows mail client (Windows 10 1809+ / Windows 11) replac
|
||||
# Build WinUI project (Debug x64)
|
||||
dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config -p:Platform=x64 -p:RuntimeIdentifier=win-x64 && dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false
|
||||
|
||||
# Build WinUI project with diagnostic XAML/compiler logging (use when plain build only shows "XamlCompiler.exe exited with code 1")
|
||||
dotnet build Wino.Mail.WinUI/Wino.Mail.WinUI.csproj -c Debug --no-restore /p:Platform=x64 /p:RuntimeIdentifier=win-x64 /p:GenerateAppxPackageOnBuild=false /p:AppxPackageSigningEnabled=false "/flp:logfile=winui-build.log;verbosity=diagnostic" /bl:winui-build.binlog
|
||||
|
||||
# Run tests (Debug x64)
|
||||
dotnet test Wino.Core.Tests/Wino.Core.Tests.csproj -c Debug /p:Platform=x64
|
||||
|
||||
@@ -36,6 +39,7 @@ dotnet restore Wino.Mail.WinUI/Wino.Mail.WinUI.csproj --configfile nuget.config
|
||||
- After the first restore, prefer `--no-restore` builds unless package or project references changed
|
||||
- Summarize long build logs and inspect only the files named in diagnostics instead of loading large logs into context
|
||||
- When the prompt already names likely files, types, or symbols, start there instead of re-mapping the repository
|
||||
- If a WinUI build only reports `XamlCompiler.exe exited with code 1`, rerun with the diagnostic logging command above and inspect the terminal output plus `winui-build.log` for real `WMC`/`WMC1121`/binding diagnostics before guessing
|
||||
|
||||
## Architecture
|
||||
|
||||
@@ -99,6 +103,7 @@ private string searchQuery = string.Empty;
|
||||
- **NEVER** create IValueConverter classes
|
||||
- WinUI 3 auto-converts bool to Visibility: `Visibility="{x:Bind IsVisible, Mode=OneWay}"`
|
||||
- Use XamlHelpers for complex conversions: `{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(Prop)}`
|
||||
- `x:Bind` does not implicitly convert `double` to `GridLength`; when binding `RowDefinition.Height` or `ColumnDefinition.Width`, use a `XamlHelpers` method such as `DoubleToGridLength(...)`
|
||||
|
||||
## Localization
|
||||
|
||||
|
||||
@@ -41,6 +41,12 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
[ObservableProperty]
|
||||
public partial int WorkingDayEndIndex { get; set; }
|
||||
|
||||
[ObservableProperty]
|
||||
public partial string TimedDayHeaderDateFormat { get; set; } = "ddd dd";
|
||||
|
||||
[ObservableProperty]
|
||||
public partial int SelectedTimedDayHeaderFormatPresetIndex { get; set; } = -1;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial List<string> ReminderOptions { get; set; } = [];
|
||||
|
||||
@@ -56,6 +62,14 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
public ObservableCollection<MailAccount> Accounts { get; } = [];
|
||||
public ObservableCollection<CalendarNewEventBehaviorOption> NewEventBehaviorOptions { get; } = [];
|
||||
public ObservableCollection<AccountCalendarViewModel> AvailableNewEventCalendars { get; } = [];
|
||||
public ObservableCollection<string> TimedDayHeaderFormatPresets { get; } =
|
||||
[
|
||||
"ddd dd",
|
||||
"dddd dd",
|
||||
"ddd d MMM",
|
||||
"dd MMM ddd",
|
||||
"M/d ddd"
|
||||
];
|
||||
|
||||
[ObservableProperty]
|
||||
public partial CalendarNewEventBehaviorOption SelectedNewEventBehaviorOption { get; set; }
|
||||
@@ -68,6 +82,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
public IPreferencesService PreferencesService { get; }
|
||||
private readonly ICalendarService _calendarService;
|
||||
private readonly IAccountService _accountService;
|
||||
private readonly CultureInfo _calendarCulture;
|
||||
private readonly bool _isLoaded = false;
|
||||
|
||||
public CalendarSettingsPageViewModel(IPreferencesService preferencesService, ICalendarService calendarService, IAccountService accountService)
|
||||
@@ -78,6 +93,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
|
||||
var currentLanguageLanguageCode = WinoTranslationDictionary.GetLanguageFileNameRelativePath(preferencesService.CurrentLanguage);
|
||||
var cultureInfo = new CultureInfo(currentLanguageLanguageCode);
|
||||
_calendarCulture = cultureInfo;
|
||||
|
||||
for (var i = 0; i < 7; i++)
|
||||
{
|
||||
@@ -92,6 +108,8 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
CellHourHeight = preferencesService.HourHeight;
|
||||
WorkingDayStartIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayStart));
|
||||
WorkingDayEndIndex = DayNames.IndexOf(cultureInfo.DateTimeFormat.GetDayName(preferencesService.WorkingDayEnd));
|
||||
TimedDayHeaderDateFormat = preferencesService.CalendarTimedDayHeaderDateFormat;
|
||||
SelectedTimedDayHeaderFormatPresetIndex = TimedDayHeaderFormatPresets.IndexOf(TimedDayHeaderDateFormat);
|
||||
|
||||
var predefinedMinutes = _calendarService.GetPredefinedReminderMinutes();
|
||||
ReminderOptions.Add("None");
|
||||
@@ -169,12 +187,52 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
}
|
||||
|
||||
partial void OnCellHourHeightChanged(double oldValue, double newValue) => SaveSettings();
|
||||
partial void OnIs24HourHeadersChanged(bool value) => SaveSettings();
|
||||
partial void OnIs24HourHeadersChanged(bool value)
|
||||
{
|
||||
OnPropertyChanged(nameof(TimedHourLabelPreview));
|
||||
SaveSettings();
|
||||
}
|
||||
partial void OnSelectedFirstDayOfWeekIndexChanged(int value) => SaveSettings();
|
||||
partial void OnWorkingHourStartChanged(TimeSpan value) => SaveSettings();
|
||||
partial void OnWorkingHourEndChanged(TimeSpan value) => SaveSettings();
|
||||
partial void OnWorkingDayStartIndexChanged(int value) => SaveSettings();
|
||||
partial void OnWorkingDayEndIndexChanged(int value) => SaveSettings();
|
||||
partial void OnTimedDayHeaderDateFormatChanged(string value)
|
||||
{
|
||||
OnPropertyChanged(nameof(TimedDayHeaderFormatPreview));
|
||||
OnPropertyChanged(nameof(TimedHourLabelPreview));
|
||||
|
||||
var normalizedFormat = string.IsNullOrWhiteSpace(value) ? "ddd dd" : value.Trim();
|
||||
var matchingPresetIndex = TimedDayHeaderFormatPresets
|
||||
.Select((format, index) => new { format, index })
|
||||
.Where(item => string.Equals(item.format, normalizedFormat, StringComparison.Ordinal))
|
||||
.Select(item => item.index)
|
||||
.DefaultIfEmpty(-1)
|
||||
.First();
|
||||
|
||||
if (SelectedTimedDayHeaderFormatPresetIndex != matchingPresetIndex)
|
||||
{
|
||||
SelectedTimedDayHeaderFormatPresetIndex = matchingPresetIndex;
|
||||
}
|
||||
|
||||
SaveSettings();
|
||||
}
|
||||
partial void OnSelectedTimedDayHeaderFormatPresetIndexChanged(int value)
|
||||
{
|
||||
if (value < 0 || value >= TimedDayHeaderFormatPresets.Count)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedPreset = TimedDayHeaderFormatPresets[value];
|
||||
|
||||
if (string.Equals(TimedDayHeaderDateFormat, selectedPreset, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TimedDayHeaderDateFormat = selectedPreset;
|
||||
}
|
||||
partial void OnSelectedDefaultReminderIndexChanged(int value) => SaveSettings();
|
||||
partial void OnSelectedDefaultSnoozeIndexChanged(int value) => SaveSettings();
|
||||
partial void OnSelectedNewEventBehaviorOptionChanged(CalendarNewEventBehaviorOption value)
|
||||
@@ -184,6 +242,49 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
}
|
||||
partial void OnSelectedNewEventCalendarChanged(AccountCalendarViewModel value) => SaveSettings();
|
||||
|
||||
public string TimedDayHeaderFormatPreview
|
||||
{
|
||||
get
|
||||
{
|
||||
var format = string.IsNullOrWhiteSpace(TimedDayHeaderDateFormat) ? "ddd dd" : TimedDayHeaderDateFormat.Trim();
|
||||
var previewDates = new[]
|
||||
{
|
||||
new DateTime(2026, 3, 23),
|
||||
new DateTime(2026, 3, 24),
|
||||
new DateTime(2026, 3, 25)
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
return string.Join(" · ", previewDates.Select(date => date.ToString(format, _calendarCulture)));
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return string.Join(" · ", previewDates.Select(date => date.ToString("ddd dd", _calendarCulture)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string TimedHourLabelPreview
|
||||
{
|
||||
get
|
||||
{
|
||||
var previewHours = new[] { 0, 9, 14, 24 };
|
||||
return string.Join(" · ", previewHours.Select(CurrentSettingsPreviewLabel));
|
||||
}
|
||||
}
|
||||
|
||||
private string CurrentSettingsPreviewLabel(int hour)
|
||||
{
|
||||
if (Is24HourHeaders)
|
||||
{
|
||||
return hour.ToString(_calendarCulture);
|
||||
}
|
||||
|
||||
var displayHour = hour % 24;
|
||||
return DateTime.Today.AddHours(displayHour).ToString("h tt", _calendarCulture);
|
||||
}
|
||||
|
||||
public void SaveSettings()
|
||||
{
|
||||
if (!_isLoaded)
|
||||
@@ -229,6 +330,7 @@ public partial class CalendarSettingsPageViewModel : CalendarBaseViewModel
|
||||
PreferencesService.WorkingHourStart = WorkingHourStart;
|
||||
PreferencesService.WorkingHourEnd = WorkingHourEnd;
|
||||
PreferencesService.HourHeight = CellHourHeight;
|
||||
PreferencesService.CalendarTimedDayHeaderDateFormat = TimedDayHeaderDateFormat;
|
||||
|
||||
if (SelectedDefaultReminderIndex == 0)
|
||||
{
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Calendar.ViewModels.Messages;
|
||||
|
||||
public class CalendarItemTappedMessage
|
||||
{
|
||||
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel, CalendarDayModel clickedPeriod)
|
||||
public CalendarItemTappedMessage(CalendarItemViewModel calendarItemViewModel)
|
||||
{
|
||||
CalendarItemViewModel = calendarItemViewModel;
|
||||
ClickedPeriod = clickedPeriod;
|
||||
}
|
||||
|
||||
public CalendarItemViewModel CalendarItemViewModel { get; }
|
||||
public CalendarDayModel ClickedPeriod { get; }
|
||||
}
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
using System.Linq;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Core.Domain.Collections;
|
||||
|
||||
public class DayRangeCollection : ObservableRangeCollection<DayRangeRenderModel>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the range of dates that are currently displayed in the collection.
|
||||
/// </summary>
|
||||
public DateRange DisplayRange
|
||||
{
|
||||
get
|
||||
{
|
||||
if (Count == 0) return null;
|
||||
|
||||
var minimumLoadedDate = this[0].CalendarRenderOptions.DateRange.StartDate;
|
||||
var maximumLoadedDate = this[Count - 1].CalendarRenderOptions.DateRange.EndDate;
|
||||
|
||||
return new DateRange(minimumLoadedDate, maximumLoadedDate);
|
||||
}
|
||||
}
|
||||
|
||||
public void RemoveCalendarItem(ICalendarItem calendarItem)
|
||||
{
|
||||
foreach (var dayRange in this)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void AddCalendarItem(ICalendarItem calendarItem)
|
||||
{
|
||||
foreach (var dayRange in this)
|
||||
{
|
||||
var calendarDayModel = dayRange.CalendarDays.FirstOrDefault(x => x.Period.HasInside(calendarItem.Period.Start));
|
||||
calendarDayModel?.EventsCollection.AddCalendarItem(calendarItem);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,6 +236,7 @@ public interface IPreferencesService : INotifyPropertyChanged
|
||||
DayOfWeek WorkingDayStart { get; set; }
|
||||
DayOfWeek WorkingDayEnd { get; set; }
|
||||
double HourHeight { get; set; }
|
||||
string CalendarTimedDayHeaderDateFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Setting: Default reminder duration in seconds for new calendar events.
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
using System;
|
||||
using Itenso.TimePeriod;
|
||||
using Wino.Core.Domain.Collections;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a day in the calendar.
|
||||
/// Can hold events, appointments, wheather status etc.
|
||||
/// </summary>
|
||||
public class CalendarDayModel
|
||||
{
|
||||
public ITimePeriod Period { get; }
|
||||
public CalendarEventCollection EventsCollection { get; }
|
||||
|
||||
public CalendarDayModel(DateTime representingDate, CalendarRenderOptions calendarRenderOptions)
|
||||
{
|
||||
RepresentingDate = representingDate;
|
||||
Period = new TimeRange(representingDate, representingDate.AddDays(1));
|
||||
CalendarRenderOptions = calendarRenderOptions;
|
||||
EventsCollection = new CalendarEventCollection(Period, calendarRenderOptions.CalendarSettings);
|
||||
}
|
||||
|
||||
public DateTime RepresentingDate { get; }
|
||||
public CalendarRenderOptions CalendarRenderOptions { get; }
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public class CalendarRenderOptions
|
||||
{
|
||||
public CalendarRenderOptions(DateRange dateRange, CalendarSettings calendarSettings)
|
||||
{
|
||||
DateRange = dateRange;
|
||||
CalendarSettings = calendarSettings;
|
||||
}
|
||||
public int TotalDayCount => DateRange.TotalDays;
|
||||
public DateRange DateRange { get; }
|
||||
public CalendarSettings CalendarSettings { get; }
|
||||
}
|
||||
@@ -13,7 +13,8 @@ public record CalendarSettings(DayOfWeek FirstDayOfWeek,
|
||||
TimeSpan WorkingHourEnd,
|
||||
double HourHeight,
|
||||
DayHeaderDisplayType DayHeaderDisplayType,
|
||||
CultureInfo CultureInfo)
|
||||
CultureInfo CultureInfo,
|
||||
string TimedDayHeaderDateFormat = "ddd dd")
|
||||
{
|
||||
public int WorkWeekDayCount
|
||||
{
|
||||
@@ -65,4 +66,35 @@ public record CalendarSettings(DayOfWeek FirstDayOfWeek,
|
||||
var dateTime = DateTime.Today.Add(timeSpan);
|
||||
return dateTime.ToString(format, CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
public string GetTimedDayHeaderText(DateOnly date)
|
||||
{
|
||||
var format = string.IsNullOrWhiteSpace(TimedDayHeaderDateFormat) ? "ddd dd" : TimedDayHeaderDateFormat;
|
||||
|
||||
try
|
||||
{
|
||||
return date.ToDateTime(TimeOnly.MinValue).ToString(format, CultureInfo);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
return date.ToDateTime(TimeOnly.MinValue).ToString("ddd dd", CultureInfo);
|
||||
}
|
||||
}
|
||||
|
||||
public string GetTimedHourLabelText(int hour)
|
||||
{
|
||||
if (hour < 0 || hour > 24)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(hour));
|
||||
}
|
||||
|
||||
if (DayHeaderDisplayType == DayHeaderDisplayType.TwentyFourHour)
|
||||
{
|
||||
return hour.ToString(CultureInfo);
|
||||
}
|
||||
|
||||
var displayHour = hour % 24;
|
||||
var dateTime = DateTime.Today.AddHours(displayHour);
|
||||
return dateTime.ToString("h tt", CultureInfo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
public class DayHeaderRenderModel
|
||||
{
|
||||
public DayHeaderRenderModel(string dayHeader, double hourHeight)
|
||||
{
|
||||
DayHeader = dayHeader;
|
||||
HourHeight = hourHeight;
|
||||
}
|
||||
|
||||
public string DayHeader { get; }
|
||||
public double HourHeight { get; }
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Itenso.TimePeriod;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a range of days in the calendar.
|
||||
/// Corresponds to 1 view of the FlipView in CalendarPage.
|
||||
/// </summary>
|
||||
public class DayRangeRenderModel
|
||||
{
|
||||
public ITimePeriod Period { get; }
|
||||
public List<CalendarDayModel> CalendarDays { get; } = [];
|
||||
|
||||
// TODO: Get rid of this at some point.
|
||||
public List<DayHeaderRenderModel> DayHeaders { get; } = [];
|
||||
public CalendarRenderOptions CalendarRenderOptions { get; }
|
||||
|
||||
public int TotalDays => CalendarRenderOptions.TotalDayCount;
|
||||
|
||||
public DayRangeRenderModel(CalendarRenderOptions calendarRenderOptions)
|
||||
{
|
||||
CalendarRenderOptions = calendarRenderOptions;
|
||||
|
||||
for (var i = 0; i < CalendarRenderOptions.TotalDayCount; i++)
|
||||
{
|
||||
var representingDate = calendarRenderOptions.DateRange.StartDate.AddDays(i);
|
||||
var calendarDayModel = new CalendarDayModel(representingDate, calendarRenderOptions);
|
||||
|
||||
CalendarDays.Add(calendarDayModel);
|
||||
}
|
||||
|
||||
Period = new TimeRange(CalendarDays.First().RepresentingDate, CalendarDays.Last().RepresentingDate.AddDays(1));
|
||||
|
||||
// Create day headers based on culture info.
|
||||
|
||||
for (var i = 0; i < 24; i++)
|
||||
{
|
||||
var representingDate = calendarRenderOptions.DateRange.StartDate.Date.AddHours(i);
|
||||
|
||||
string dayHeader = calendarRenderOptions.CalendarSettings.DayHeaderDisplayType switch
|
||||
{
|
||||
DayHeaderDisplayType.TwelveHour => representingDate.ToString("h tt", calendarRenderOptions.CalendarSettings.CultureInfo),
|
||||
DayHeaderDisplayType.TwentyFourHour => representingDate.ToString("HH", calendarRenderOptions.CalendarSettings.CultureInfo),
|
||||
_ => "N/A"
|
||||
};
|
||||
|
||||
DayHeaders.Add(new DayHeaderRenderModel(dayHeader, calendarRenderOptions.CalendarSettings.HourHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -731,6 +731,8 @@
|
||||
"SettingsCalendarSettings_Title": "Calendar Settings",
|
||||
"CalendarSettings_DefaultSnoozeDuration_Header": "Default snooze duration",
|
||||
"CalendarSettings_DefaultSnoozeDuration_Description": "Set a default snooze duration for calendar reminder notifications.",
|
||||
"CalendarSettings_TimedDayHeaderFormat_Header": "Timed view day header format",
|
||||
"CalendarSettings_TimedDayHeaderFormat_Description": "Choose how the top day labels are rendered in day, week, and work week views. Use date format tokens like ddd, dd, MMM, or dddd.",
|
||||
"SettingsComposer_Title": "Composer",
|
||||
"SettingsComposerFont_Title": "Default Composer Font",
|
||||
"SettingsComposerFontFamily_Description": "Change the default font family and font size for composing mails.",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Windows.Foundation;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public sealed class CalendarEmptySlotTappedEventArgs : EventArgs
|
||||
{
|
||||
public CalendarEmptySlotTappedEventArgs(DateTime clickedDate, Point positionerPoint, Size cellSize)
|
||||
{
|
||||
ClickedDate = clickedDate;
|
||||
PositionerPoint = positionerPoint;
|
||||
CellSize = cellSize;
|
||||
}
|
||||
|
||||
public DateTime ClickedDate { get; }
|
||||
public Point PositionerPoint { get; }
|
||||
public Size CellSize { get; }
|
||||
}
|
||||
@@ -9,8 +9,6 @@ using Microsoft.UI.Xaml.Media;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Calendar.ViewModels.Messages;
|
||||
using Wino.Core.Domain;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public sealed partial class CalendarItemControl : UserControl
|
||||
@@ -96,7 +94,7 @@ public sealed partial class CalendarItemControl : UserControl
|
||||
|
||||
if (isSingleTap && CalendarItem != null)
|
||||
{
|
||||
WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem, null));
|
||||
WeakReferenceMessenger.Default.Send(new CalendarItemTappedMessage(CalendarItem));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:helpers="using:Wino.Helpers"
|
||||
xmlns:local="using:Wino.Calendar.Controls"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:skia="using:SkiaSharp.Views.Windows"
|
||||
@@ -53,14 +54,34 @@
|
||||
<Grid>
|
||||
<Grid x:Name="TimedRoot" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="44" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="64" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Grid x:Name="TimedHeaderHost">
|
||||
<Border
|
||||
x:Name="TimedHourHeaderHost"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Height="44"
|
||||
Background="{ThemeResource LayerFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,1,1" />
|
||||
|
||||
<Grid
|
||||
x:Name="TimedHeaderHost"
|
||||
Grid.Row="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Margin="64,0,0,0"
|
||||
Height="44"
|
||||
Background="{ThemeResource LayerFillColorDefaultBrush}">
|
||||
<skia:SKXamlCanvas x:Name="TimedHeaderCanvas" PaintSurface="TimedHeaderCanvasPaintSurface" />
|
||||
<ItemsControl
|
||||
x:Name="TimedHeadersItemsControl"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
ItemTemplate="{StaticResource TimedHeaderTemplate}"
|
||||
ItemsSource="{x:Bind TimedHeaderTexts, Mode=OneWay}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
@@ -69,19 +90,44 @@
|
||||
</ItemsControl>
|
||||
</Grid>
|
||||
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Grid.ColumnSpan="2"
|
||||
Background="Transparent">
|
||||
<ScrollViewer
|
||||
x:Name="TimedScrollViewer"
|
||||
Grid.Row="1"
|
||||
Background="Transparent"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
VerticalScrollMode="Enabled">
|
||||
<Grid x:Name="TimedViewport" Height="{x:Bind TimelineHeight, Mode=OneWay}">
|
||||
<Grid x:Name="TimedScrollContentGrid" Height="{x:Bind TimelineHeight, Mode=OneWay}">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="64" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<Canvas
|
||||
x:Name="HourLabelsCanvas"
|
||||
Width="64"
|
||||
IsHitTestVisible="False" />
|
||||
|
||||
<Grid
|
||||
x:Name="TimedViewport"
|
||||
Grid.Column="1"
|
||||
Height="{x:Bind TimelineHeight, Mode=OneWay}">
|
||||
<skia:SKXamlCanvas x:Name="TimedStructureCanvas" PaintSurface="TimedStructureCanvasPaintSurface" />
|
||||
<Border
|
||||
x:Name="TimedInteractionLayer"
|
||||
Background="Transparent"
|
||||
Tapped="TimedInteractionLayerTapped" />
|
||||
<Canvas x:Name="TimedItemsCanvas" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
</Grid>
|
||||
|
||||
<Grid x:Name="MonthRoot" Visibility="Collapsed">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
@@ -106,7 +152,11 @@
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch">
|
||||
<skia:SKXamlCanvas x:Name="MonthStructureCanvas" PaintSurface="MonthStructureCanvasPaintSurface" />
|
||||
<Canvas x:Name="MonthCellLabelsCanvas" />
|
||||
<Border
|
||||
x:Name="MonthInteractionLayer"
|
||||
Background="Transparent"
|
||||
Tapped="MonthInteractionLayerTapped" />
|
||||
<Canvas x:Name="MonthCellLabelsCanvas" IsHitTestVisible="False" />
|
||||
<Canvas x:Name="MonthItemsCanvas" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -12,9 +12,11 @@ using Microsoft.UI.Composition;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Hosting;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
using Microsoft.UI.Xaml.Media;
|
||||
using SkiaSharp;
|
||||
using SkiaSharp.Views.Windows;
|
||||
using Windows.Foundation;
|
||||
using Windows.UI;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain.Enums;
|
||||
@@ -25,6 +27,9 @@ namespace Wino.Calendar.Controls;
|
||||
|
||||
public sealed partial class CalendarPeriodControl : UserControl, INotifyPropertyChanged
|
||||
{
|
||||
private const double TimedHourColumnWidth = 64d;
|
||||
private const double TimedGridIntervalMinutes = 30d;
|
||||
private const double TimedSelectionIntervalMinutes = 30d;
|
||||
private VisibleDateRange _currentRange = new(
|
||||
CalendarDisplayType.Month,
|
||||
DateOnly.FromDateTime(DateTime.Today),
|
||||
@@ -62,6 +67,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
public CalendarPeriodControl() => InitializeComponent();
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
public event EventHandler<CalendarEmptySlotTappedEventArgs>? EmptySlotTapped;
|
||||
|
||||
private ObservableCollection<HeaderTextLayout> TimedHeaderTextsCollection { get; } = [];
|
||||
private ObservableCollection<HeaderTextLayout> MonthHeaderTextsCollection { get; } = [];
|
||||
@@ -209,20 +215,22 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
{
|
||||
TimedRoot.Visibility = Visibility.Visible;
|
||||
MonthRoot.Visibility = Visibility.Collapsed;
|
||||
ResetTimedVisualState();
|
||||
|
||||
TimedDayWidth = _currentRange.Dates.Count == 0 ? 0d : ActualWidth / _currentRange.Dates.Count;
|
||||
TimedViewport.Width = ActualWidth;
|
||||
var timedSurfaceWidth = GetTimedSurfaceWidth();
|
||||
|
||||
TimedDayWidth = _currentRange.Dates.Count == 0 ? 0d : timedSurfaceWidth / _currentRange.Dates.Count;
|
||||
TimedScrollContentGrid.Width = ActualWidth;
|
||||
TimedViewport.Width = timedSurfaceWidth;
|
||||
TimedViewport.Height = TimelineHeight;
|
||||
|
||||
_timedLayout = TimedCalendarLayoutCalculator.Calculate(_currentRange, CurrentItems, ActualWidth, GetHourHeight());
|
||||
_timedLayout = TimedCalendarLayoutCalculator.Calculate(_currentRange, CurrentItems, timedSurfaceWidth, GetHourHeight());
|
||||
|
||||
ReplaceCollection(
|
||||
TimedHeaderTextsCollection,
|
||||
_timedLayout.VisibleDates.Select(date =>
|
||||
new HeaderTextLayout(
|
||||
date.ToDateTime(TimeOnly.MinValue).ToString(
|
||||
string.IsNullOrWhiteSpace(TimedHeaderDateFormat) ? "ddd dd" : TimedHeaderDateFormat,
|
||||
CalendarSettings!.CultureInfo),
|
||||
GetTimedHeaderText(date),
|
||||
TimedDayWidth)));
|
||||
|
||||
var eventTemplate = (DataTemplate)Resources["CalendarEventTemplate"];
|
||||
@@ -232,6 +240,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
item.Template = eventTemplate;
|
||||
return item;
|
||||
}));
|
||||
RenderHourLabels();
|
||||
RenderTimedItems();
|
||||
|
||||
TimedHeaderCanvas.Invalidate();
|
||||
@@ -352,12 +361,14 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
var canvas = e.Surface.Canvas;
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
if (_timedLayout.VisibleDates.Count == 0 || ActualWidth <= 0)
|
||||
var timedSurfaceWidth = GetTimedSurfaceWidth();
|
||||
|
||||
if (_timedLayout.VisibleDates.Count == 0 || timedSurfaceWidth <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scaleX = (float)(e.Info.Width / ActualWidth);
|
||||
var scaleX = (float)(e.Info.Width / timedSurfaceWidth);
|
||||
var height = e.Info.Height;
|
||||
var dayWidth = (float)(_timedLayout.DayWidth * scaleX);
|
||||
|
||||
@@ -373,41 +384,50 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
private void TimedStructureCanvasPaintSurface(object? sender, SKPaintSurfaceEventArgs e)
|
||||
{
|
||||
using var linePaint = CreateLinePaint();
|
||||
using var minorLinePaint = CreateMinorLinePaint();
|
||||
using var defaultFillPaint = CreateFillPaint(GetDefaultHourBackground());
|
||||
using var workFillPaint = CreateFillPaint(GetWorkHourBackground());
|
||||
var canvas = e.Surface.Canvas;
|
||||
canvas.Clear(SKColors.Transparent);
|
||||
|
||||
if (_timedLayout.VisibleDates.Count == 0 || ActualWidth <= 0)
|
||||
var timedSurfaceWidth = GetTimedSurfaceWidth();
|
||||
|
||||
if (_timedLayout.VisibleDates.Count == 0 || timedSurfaceWidth <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var hourHeight = GetHourHeight();
|
||||
var timelineHeight = TimedCalendarLayoutCalculator.GetTimelineHeight(hourHeight);
|
||||
var scaleX = (float)(e.Info.Width / ActualWidth);
|
||||
var scaleX = (float)(e.Info.Width / timedSurfaceWidth);
|
||||
var scaleY = (float)(e.Info.Height / timelineHeight);
|
||||
var dayWidth = (float)(_timedLayout.DayWidth * scaleX);
|
||||
var workDayStartHour = CalendarSettings?.WorkingHourStart.TotalHours ?? 9d;
|
||||
var workDayEndHour = CalendarSettings?.WorkingHourEnd.TotalHours ?? 17d;
|
||||
var intervalHeight = (float)(GetTimedGridIntervalHeight() * scaleY);
|
||||
var intervalCount = (int)(24d * 60d / TimedGridIntervalMinutes);
|
||||
|
||||
for (var dayIndex = 0; dayIndex < _timedLayout.VisibleDates.Count; dayIndex++)
|
||||
{
|
||||
var x = dayIndex * dayWidth;
|
||||
var isWorkingDay = CalendarSettings?.WorkingDays.Contains(_timedLayout.VisibleDates[dayIndex].DayOfWeek) == true;
|
||||
|
||||
for (var hour = 0; hour < 24; hour++)
|
||||
for (var intervalIndex = 0; intervalIndex < intervalCount; intervalIndex++)
|
||||
{
|
||||
var y = (float)(hour * hourHeight * scaleY);
|
||||
var scaledHourHeight = (float)(hourHeight * scaleY);
|
||||
var fillPaint = hour >= workDayStartHour && hour < workDayEndHour ? workFillPaint : defaultFillPaint;
|
||||
canvas.DrawRect(x, y, dayWidth, scaledHourHeight, fillPaint);
|
||||
var intervalStartHour = (intervalIndex * TimedGridIntervalMinutes) / 60d;
|
||||
var y = intervalIndex * intervalHeight;
|
||||
var fillPaint = isWorkingDay && intervalStartHour >= workDayStartHour && intervalStartHour < workDayEndHour
|
||||
? workFillPaint
|
||||
: defaultFillPaint;
|
||||
canvas.DrawRect(x, y, dayWidth, intervalHeight, fillPaint);
|
||||
}
|
||||
}
|
||||
|
||||
for (var hour = 0; hour <= 24; hour++)
|
||||
for (var intervalIndex = 0; intervalIndex <= intervalCount; intervalIndex++)
|
||||
{
|
||||
var y = (float)(hour * hourHeight * scaleY);
|
||||
canvas.DrawLine(0, y, e.Info.Width, y, linePaint);
|
||||
var y = intervalIndex * intervalHeight;
|
||||
var paint = intervalIndex % 2 == 0 ? linePaint : minorLinePaint;
|
||||
canvas.DrawLine(0, y, e.Info.Width, y, paint);
|
||||
}
|
||||
|
||||
for (var index = 0; index <= _timedLayout.VisibleDates.Count; index++)
|
||||
@@ -472,6 +492,34 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderHourLabels()
|
||||
{
|
||||
HourLabelsCanvas.Children.Clear();
|
||||
HourLabelsCanvas.Height = TimelineHeight;
|
||||
|
||||
var hourHeight = GetHourHeight();
|
||||
var labelWidth = Math.Max(0d, TimedHourColumnWidth - 10d);
|
||||
|
||||
for (var hour = 0; hour <= 24; hour++)
|
||||
{
|
||||
var textBlock = new TextBlock
|
||||
{
|
||||
Width = labelWidth,
|
||||
Text = GetTimedHourLabelText(hour),
|
||||
TextAlignment = TextAlignment.Right,
|
||||
Opacity = 0.72
|
||||
};
|
||||
|
||||
var y = hour == 24
|
||||
? Math.Max(0d, TimelineHeight - 20d)
|
||||
: Math.Max(0d, (hour * hourHeight) - 10d);
|
||||
|
||||
Canvas.SetLeft(textBlock, 0d);
|
||||
Canvas.SetTop(textBlock, y);
|
||||
HourLabelsCanvas.Children.Add(textBlock);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderTimedItems()
|
||||
{
|
||||
TimedItemsCanvas.Children.Clear();
|
||||
@@ -532,6 +580,70 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
}
|
||||
}
|
||||
|
||||
private void TimedInteractionLayerTapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
if (_timedLayout.VisibleDates.Count == 0 || _timedLayout.DayWidth <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var position = e.GetPosition(TimedViewport);
|
||||
var dayIndex = Math.Clamp((int)(position.X / _timedLayout.DayWidth), 0, _timedLayout.VisibleDates.Count - 1);
|
||||
var intervalHeight = GetTimedSelectionIntervalHeight();
|
||||
var slotIndex = Math.Clamp((int)(position.Y / intervalHeight), 0, (int)((24d * 60d / TimedSelectionIntervalMinutes) - 1));
|
||||
var slotStart = TimeSpan.FromMinutes(slotIndex * TimedSelectionIntervalMinutes);
|
||||
var clickedDate = _timedLayout.VisibleDates[dayIndex].ToDateTime(TimeOnly.MinValue).Add(slotStart);
|
||||
|
||||
EmptySlotTapped?.Invoke(
|
||||
this,
|
||||
new CalendarEmptySlotTappedEventArgs(
|
||||
clickedDate,
|
||||
new Point(dayIndex * _timedLayout.DayWidth, slotIndex * intervalHeight),
|
||||
new Size(_timedLayout.DayWidth, intervalHeight)));
|
||||
}
|
||||
|
||||
private void MonthInteractionLayerTapped(object sender, TappedRoutedEventArgs e)
|
||||
{
|
||||
if (_monthLayout.Cells.Count == 0 || _monthLayout.CellWidth <= 0 || _monthLayout.CellHeight <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var position = e.GetPosition(MonthViewport);
|
||||
var column = Math.Clamp((int)(position.X / _monthLayout.CellWidth), 0, MonthCalendarLayoutCalculator.ColumnCount - 1);
|
||||
var row = Math.Clamp((int)(position.Y / _monthLayout.CellHeight), 0, MonthCalendarLayoutCalculator.RowCount - 1);
|
||||
var cellIndex = Math.Clamp((row * MonthCalendarLayoutCalculator.ColumnCount) + column, 0, _monthLayout.Cells.Count - 1);
|
||||
var cell = _monthLayout.Cells[cellIndex];
|
||||
|
||||
EmptySlotTapped?.Invoke(
|
||||
this,
|
||||
new CalendarEmptySlotTappedEventArgs(
|
||||
cell.Date.ToDateTime(TimeOnly.MinValue),
|
||||
new Point(cell.Bounds.X, cell.Bounds.Y),
|
||||
new Size(cell.Bounds.Width, cell.Bounds.Height)));
|
||||
}
|
||||
|
||||
private double GetTimedSurfaceWidth() => Math.Max(0d, ActualWidth - TimedHourColumnWidth);
|
||||
|
||||
private string GetTimedHeaderText(DateOnly date)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(TimedHeaderDateFormat) && CalendarSettings is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return date.ToDateTime(TimeOnly.MinValue).ToString(TimedHeaderDateFormat, CalendarSettings.CultureInfo);
|
||||
}
|
||||
catch (FormatException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
return CalendarSettings?.GetTimedDayHeaderText(date) ?? date.ToDateTime(TimeOnly.MinValue).ToString("ddd dd");
|
||||
}
|
||||
|
||||
private string GetTimedHourLabelText(int hour)
|
||||
=> CalendarSettings?.GetTimedHourLabelText(hour) ?? $"{hour:00}:00";
|
||||
|
||||
private CalendarTransitionInfo GetTransitionInfo()
|
||||
{
|
||||
if (!_hasPresentedState || VisibleRange is null || CalendarSettings is null)
|
||||
@@ -597,11 +709,9 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
|
||||
private void RunTimedTransition(CalendarTransitionInfo transition)
|
||||
{
|
||||
var headerVisual = ElementCompositionPreview.GetElementVisual(TimedHeaderHost);
|
||||
var contentVisual = ElementCompositionPreview.GetElementVisual(TimedScrollViewer);
|
||||
var compositor = headerVisual.Compositor;
|
||||
var compositor = contentVisual.Compositor;
|
||||
|
||||
PrepareAnimatedVisual(headerVisual, TimedHeaderHost);
|
||||
PrepareAnimatedVisual(contentVisual, TimedScrollViewer);
|
||||
|
||||
switch (transition.Kind)
|
||||
@@ -618,6 +728,11 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
}
|
||||
}
|
||||
|
||||
private void ResetTimedVisualState()
|
||||
{
|
||||
ResetAnimatedElement(TimedScrollViewer);
|
||||
}
|
||||
|
||||
private static void StartNavigationTransition(Compositor compositor, Visual visual, int direction, double width)
|
||||
{
|
||||
var travel = (float)Math.Max(48d, Math.Min(160d, width * 0.08d));
|
||||
@@ -644,8 +759,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
var signedTravel = direction >= 0 ? travel : -travel;
|
||||
var clipInset = (float)Math.Max(18d, Math.Min(64d, width * 0.05d));
|
||||
|
||||
StartTimedElementTransition(compositor, TimedHeaderHost, signedTravel * 0.45f, 0f, 0.78f, TimeSpan.FromMilliseconds(180), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f);
|
||||
StartTimedElementTransition(compositor, TimedScrollViewer, signedTravel, 0f, 0.68f, TimeSpan.FromMilliseconds(240), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f);
|
||||
StartTimedElementTransition(compositor, TimedScrollViewer, signedTravel, 0f, 0.68f, TimeSpan.FromMilliseconds(240), direction >= 0 ? 0f : clipInset, direction >= 0 ? clipInset : 0f, animateScale: false);
|
||||
}
|
||||
|
||||
private static void StartModeTransition(Compositor compositor, Visual visual)
|
||||
@@ -672,8 +786,7 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
|
||||
private void StartTimedModeTransition(Compositor compositor)
|
||||
{
|
||||
StartTimedElementTransition(compositor, TimedHeaderHost, 0f, 10f, 0f, TimeSpan.FromMilliseconds(180), 0f, 0f);
|
||||
StartTimedElementTransition(compositor, TimedScrollViewer, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f);
|
||||
StartTimedElementTransition(compositor, TimedScrollViewer, 0f, 18f, 0f, TimeSpan.FromMilliseconds(240), 0f, 0f, animateScale: false);
|
||||
}
|
||||
|
||||
private static void StartRefreshTransition(Compositor compositor, Visual visual)
|
||||
@@ -688,7 +801,6 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
|
||||
private void StartTimedRefreshTransition(Compositor compositor)
|
||||
{
|
||||
StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedHeaderHost), 0.86f, TimeSpan.FromMilliseconds(140));
|
||||
StartOpacityTransition(compositor, ElementCompositionPreview.GetElementVisual(TimedScrollViewer), 0.8f, TimeSpan.FromMilliseconds(160));
|
||||
}
|
||||
|
||||
@@ -700,7 +812,25 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
visual.StopAnimation(nameof(visual.Scale));
|
||||
}
|
||||
|
||||
private static void StartTimedElementTransition(Compositor compositor, UIElement target, float offsetX, float offsetY, float startingOpacity, TimeSpan duration, float leftInset, float rightInset)
|
||||
private static void ResetAnimatedElement(UIElement target)
|
||||
{
|
||||
var visual = ElementCompositionPreview.GetElementVisual(target);
|
||||
PrepareAnimatedVisual(visual, target);
|
||||
|
||||
visual.Offset = Vector3.Zero;
|
||||
visual.Opacity = 1f;
|
||||
visual.Scale = new Vector3(1f, 1f, 1f);
|
||||
|
||||
if (visual.Clip is InsetClip clip)
|
||||
{
|
||||
clip.StopAnimation(nameof(clip.LeftInset));
|
||||
clip.StopAnimation(nameof(clip.RightInset));
|
||||
clip.LeftInset = 0f;
|
||||
clip.RightInset = 0f;
|
||||
}
|
||||
}
|
||||
|
||||
private static void StartTimedElementTransition(Compositor compositor, UIElement target, float offsetX, float offsetY, float startingOpacity, TimeSpan duration, float leftInset, float rightInset, bool animateScale = true)
|
||||
{
|
||||
var visual = ElementCompositionPreview.GetElementVisual(target);
|
||||
PrepareAnimatedVisual(visual, target);
|
||||
@@ -723,11 +853,6 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
opacityAnimation.InsertKeyFrame(1f, 1f, fadeEasing);
|
||||
opacityAnimation.Duration = duration;
|
||||
|
||||
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
|
||||
scaleAnimation.InsertKeyFrame(0f, new Vector3(0.996f, 0.996f, 1f));
|
||||
scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1f), easing);
|
||||
scaleAnimation.Duration = duration;
|
||||
|
||||
var leftInsetAnimation = compositor.CreateScalarKeyFrameAnimation();
|
||||
leftInsetAnimation.InsertKeyFrame(1f, 0f, easing);
|
||||
leftInsetAnimation.Duration = duration;
|
||||
@@ -738,7 +863,20 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
|
||||
visual.StartAnimation(nameof(visual.Offset), offsetAnimation);
|
||||
visual.StartAnimation(nameof(visual.Opacity), opacityAnimation);
|
||||
|
||||
if (animateScale)
|
||||
{
|
||||
var scaleAnimation = compositor.CreateVector3KeyFrameAnimation();
|
||||
scaleAnimation.InsertKeyFrame(0f, new Vector3(0.996f, 0.996f, 1f));
|
||||
scaleAnimation.InsertKeyFrame(1f, new Vector3(1f, 1f, 1f), easing);
|
||||
scaleAnimation.Duration = duration;
|
||||
visual.StartAnimation(nameof(visual.Scale), scaleAnimation);
|
||||
}
|
||||
else
|
||||
{
|
||||
visual.Scale = new Vector3(1f, 1f, 1f);
|
||||
}
|
||||
|
||||
clip.StartAnimation(nameof(clip.LeftInset), leftInsetAnimation);
|
||||
clip.StartAnimation(nameof(clip.RightInset), rightInsetAnimation);
|
||||
}
|
||||
@@ -768,6 +906,18 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
};
|
||||
}
|
||||
|
||||
private static SKPaint CreateMinorLinePaint()
|
||||
{
|
||||
var strokeColor = GetStrokeColor();
|
||||
|
||||
return new SKPaint
|
||||
{
|
||||
Color = new SKColor(strokeColor.R, strokeColor.G, strokeColor.B, (byte)Math.Max(20, strokeColor.A / 4)),
|
||||
IsAntialias = false,
|
||||
StrokeWidth = 1
|
||||
};
|
||||
}
|
||||
|
||||
private static SKPaint CreateFillPaint(Brush brush)
|
||||
{
|
||||
return new SKPaint
|
||||
@@ -805,6 +955,12 @@ public sealed partial class CalendarPeriodControl : UserControl, INotifyProperty
|
||||
return new SolidColorBrush(Color.FromArgb(255, 34, 40, 52));
|
||||
}
|
||||
|
||||
private static double GetTimedGridIntervalHeight(double hourHeight) => hourHeight * (TimedGridIntervalMinutes / 60d);
|
||||
|
||||
private double GetTimedGridIntervalHeight() => GetTimedGridIntervalHeight(GetHourHeight());
|
||||
|
||||
private double GetTimedSelectionIntervalHeight() => GetHourHeight() * (TimedSelectionIntervalMinutes / 60d);
|
||||
|
||||
private double GetHourHeight() => CalendarSettings?.HourHeight ?? 60d;
|
||||
|
||||
private static Color GetStrokeColor()
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
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;
|
||||
|
||||
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);
|
||||
private const string NotTodayState = nameof(NotTodayState);
|
||||
|
||||
private TextBlock? HeaderDateDayText;
|
||||
private TextBlock? ColumnHeaderText;
|
||||
private Border? IsTodayBorder;
|
||||
private ItemsControl? AllDayItemsControl;
|
||||
private CalendarEventCollection? _boundEventsCollection;
|
||||
|
||||
public CalendarDayModel DayModel
|
||||
{
|
||||
get { return (CalendarDayModel)GetValue(DayModelProperty); }
|
||||
set { SetValue(DayModelProperty, value); }
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
HeaderDateDayText = GetTemplateChild(PART_HeaderDateDayText) as TextBlock;
|
||||
ColumnHeaderText = GetTemplateChild(PART_ColumnHeaderText) as TextBlock;
|
||||
IsTodayBorder = GetTemplateChild(PART_IsTodayBorder) as Border;
|
||||
AllDayItemsControl = GetTemplateChild(PART_AllDayItemsControl) as ItemsControl;
|
||||
|
||||
RegisterEventsCollectionHandlers();
|
||||
UpdateValues();
|
||||
}
|
||||
|
||||
private static void OnRenderingPropertiesChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
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 (DayModel == null) return;
|
||||
|
||||
if (HeaderDateDayText != null)
|
||||
{
|
||||
HeaderDateDayText.Text = DayModel.RepresentingDate.Day.ToString();
|
||||
}
|
||||
|
||||
// Monthly template does not use it.
|
||||
if (ColumnHeaderText != null)
|
||||
{
|
||||
ColumnHeaderText.Text = DayModel.RepresentingDate.ToString("dddd", DayModel.CalendarRenderOptions.CalendarSettings.CultureInfo);
|
||||
}
|
||||
|
||||
UpdateEventItemsSource();
|
||||
|
||||
if (IsTodayBorder == null) return;
|
||||
bool isToday = DayModel.RepresentingDate.Date == DateTime.Now.Date;
|
||||
|
||||
VisualStateManager.GoToState(this, isToday ? TodayState : NotTodayState, false);
|
||||
|
||||
UpdateLayout();
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain.Enums;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class DayHeaderControl : Control
|
||||
{
|
||||
private const string PART_DayHeaderTextBlock = nameof(PART_DayHeaderTextBlock);
|
||||
private TextBlock? HeaderTextblock;
|
||||
|
||||
public DayHeaderDisplayType DisplayType
|
||||
{
|
||||
get { return (DayHeaderDisplayType)GetValue(DisplayTypeProperty); }
|
||||
set { SetValue(DisplayTypeProperty, value); }
|
||||
}
|
||||
|
||||
public DateTime Date
|
||||
{
|
||||
get { return (DateTime)GetValue(DateProperty); }
|
||||
set { SetValue(DateProperty, value); }
|
||||
}
|
||||
|
||||
public static readonly DependencyProperty DateProperty = DependencyProperty.Register(nameof(Date), typeof(DateTime), typeof(DayHeaderControl), new PropertyMetadata(default(DateTime), new PropertyChangedCallback(OnHeaderPropertyChanged)));
|
||||
public static readonly DependencyProperty DisplayTypeProperty = DependencyProperty.Register(nameof(DisplayType), typeof(DayHeaderDisplayType), typeof(DayHeaderControl), new PropertyMetadata(DayHeaderDisplayType.TwentyFourHour, new PropertyChangedCallback(OnHeaderPropertyChanged)));
|
||||
|
||||
public DayHeaderControl()
|
||||
{
|
||||
DefaultStyleKey = typeof(DayHeaderControl);
|
||||
}
|
||||
|
||||
protected override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
HeaderTextblock = GetTemplateChild(PART_DayHeaderTextBlock) as TextBlock;
|
||||
UpdateHeaderText();
|
||||
}
|
||||
|
||||
private static void OnHeaderPropertyChanged(DependencyObject control, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
if (control is DayHeaderControl headerControl)
|
||||
{
|
||||
headerControl.UpdateHeaderText();
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateHeaderText()
|
||||
{
|
||||
if (HeaderTextblock != null)
|
||||
{
|
||||
HeaderTextblock.Text = DisplayType == DayHeaderDisplayType.TwelveHour ? Date.ToString("h tt") : Date.ToString("HH:mm");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Calendar.Controls;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
|
||||
namespace Wino.Mail.WinUI.Controls.Calendar;
|
||||
|
||||
/// <summary>
|
||||
/// AOT-Safe ItemsControl for use in UniformGrid panels.
|
||||
/// </summary>
|
||||
///
|
||||
public partial class UniformItemsControl : Grid
|
||||
{
|
||||
[GeneratedDependencyProperty]
|
||||
public partial DayRangeRenderModel? RenderModel { get; set; }
|
||||
|
||||
[GeneratedDependencyProperty]
|
||||
public partial List<CalendarDayModel>? ItemsSource { get; set; }
|
||||
|
||||
partial void OnRenderModelChanged(DayRangeRenderModel? newValue)
|
||||
{
|
||||
if (newValue == null || ItemsSource == null) return;
|
||||
|
||||
AdjustColumns();
|
||||
}
|
||||
|
||||
partial void OnItemsSourceChanged(List<CalendarDayModel>? newValue)
|
||||
{
|
||||
if (newValue == null || ItemsSource == null) return;
|
||||
|
||||
AdjustColumns();
|
||||
}
|
||||
|
||||
private void AdjustColumns()
|
||||
{
|
||||
if (RenderModel == null || ItemsSource == null) return;
|
||||
|
||||
Children.Clear();
|
||||
ColumnDefinitions.Clear();
|
||||
|
||||
var columns = RenderModel.TotalDays;
|
||||
|
||||
// First divide.
|
||||
for (int i = 0; i < columns; i++)
|
||||
{
|
||||
ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
|
||||
}
|
||||
|
||||
// Then add items.
|
||||
for (int i = 0; i < columns; i++)
|
||||
{
|
||||
var item = ItemsSource[i];
|
||||
|
||||
var control = new DayColumnControl()
|
||||
{
|
||||
DayModel = item
|
||||
};
|
||||
|
||||
SetColumn(control, i);
|
||||
Children.Add(control);
|
||||
}
|
||||
}
|
||||
}
|
||||
//public partial class UniformItemsControl : ItemsControl
|
||||
//{
|
||||
// private const string ControlUniformGridName = "PART_UniformGrid";
|
||||
|
||||
// [GeneratedDependencyProperty]
|
||||
// public partial DayRangeRenderModel? RenderModel { get; set; }
|
||||
|
||||
// partial void OnRenderModelChanged(DayRangeRenderModel? newValue)
|
||||
// {
|
||||
// if (newValue == null) return;
|
||||
|
||||
// // Adjust the ItemsPanel based on the RenderModel's columns.
|
||||
// var uniGrid = WinoVisualTreeHelper.FindDescendants<UniformGrid>(this);
|
||||
|
||||
// //if (uniGrid != null)
|
||||
// //{
|
||||
// // uniGrid.Columns = newValue.TotalDays;
|
||||
// //}
|
||||
// }
|
||||
//}
|
||||
@@ -1,293 +0,0 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CommunityToolkit.WinUI;
|
||||
using Itenso.TimePeriod;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Windows.Foundation;
|
||||
using Wino.Calendar.Models;
|
||||
using Wino.Calendar.ViewModels.Data;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
public partial class WinoCalendarPanel : Panel
|
||||
{
|
||||
private const double LastItemRightExtraMargin = 12d;
|
||||
|
||||
// Store each ICalendarItem measurements by their Id.
|
||||
private readonly Dictionary<ICalendarItem, CalendarItemMeasurement> _measurements = new Dictionary<ICalendarItem, CalendarItemMeasurement>();
|
||||
|
||||
public static readonly DependencyProperty EventItemMarginProperty = DependencyProperty.Register(nameof(EventItemMargin), typeof(Thickness), typeof(WinoCalendarPanel), new PropertyMetadata(new Thickness(0, 0, 0, 0)));
|
||||
public static readonly DependencyProperty HourHeightProperty = DependencyProperty.Register(nameof(HourHeight), typeof(double), typeof(WinoCalendarPanel), new PropertyMetadata(0d));
|
||||
public static readonly DependencyProperty PeriodProperty = DependencyProperty.Register(nameof(Period), typeof(ITimePeriod), typeof(WinoCalendarPanel), new PropertyMetadata(null));
|
||||
|
||||
public ITimePeriod Period
|
||||
{
|
||||
get { return (ITimePeriod)GetValue(PeriodProperty); }
|
||||
set { SetValue(PeriodProperty, value); }
|
||||
}
|
||||
|
||||
public double HourHeight
|
||||
{
|
||||
get { return (double)GetValue(HourHeightProperty); }
|
||||
set { SetValue(HourHeightProperty, value); }
|
||||
}
|
||||
|
||||
public Thickness EventItemMargin
|
||||
{
|
||||
get { return (Thickness)GetValue(EventItemMarginProperty); }
|
||||
set { SetValue(EventItemMarginProperty, value); }
|
||||
}
|
||||
|
||||
private void ResetMeasurements() => _measurements.Clear();
|
||||
|
||||
private double GetChildTopMargin(ICalendarItem calendarItemViewModel, double availableHeight)
|
||||
{
|
||||
var childStart = calendarItemViewModel.StartDate;
|
||||
|
||||
if (childStart <= Period.Start)
|
||||
{
|
||||
// Event started before or exactly at the periods tart. This might be a multi-day event.
|
||||
// We can simply consider event must not have a top margin.
|
||||
|
||||
return 0d;
|
||||
}
|
||||
|
||||
double minutesFromStart = (childStart - Period.Start).TotalMinutes;
|
||||
return (minutesFromStart / 1440) * availableHeight;
|
||||
}
|
||||
|
||||
private double GetChildWidth(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
|
||||
{
|
||||
return (calendarItemMeasurement.Right - calendarItemMeasurement.Left) * availableWidth;
|
||||
}
|
||||
|
||||
private double GetChildLeftMargin(CalendarItemMeasurement calendarItemMeasurement, double availableWidth)
|
||||
=> availableWidth * calendarItemMeasurement.Left;
|
||||
|
||||
private double GetChildHeight(ICalendarItem child)
|
||||
{
|
||||
// All day events are not measured.
|
||||
if (child.IsAllDayEvent) return 0;
|
||||
|
||||
double childDurationInMinutes = 0d;
|
||||
double availableHeight = HourHeight * 24;
|
||||
|
||||
var periodRelation = child.Period.GetRelation(Period);
|
||||
|
||||
// Debug.WriteLine($"Render relation of {child.Title} ({child.Period.Start} - {child.Period.End}) is {periodRelation} with {Period.Start.Day}");
|
||||
|
||||
if (!child.IsMultiDayEvent)
|
||||
{
|
||||
childDurationInMinutes = child.Period.Duration.TotalMinutes;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Multi-day event.
|
||||
// Check how many of the event falls into the current period.
|
||||
childDurationInMinutes = (child.Period.End - Period.Start).TotalMinutes;
|
||||
}
|
||||
|
||||
return (childDurationInMinutes / 1440) * availableHeight;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize)
|
||||
{
|
||||
ResetMeasurements();
|
||||
return base.MeasureOverride(availableSize);
|
||||
}
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize)
|
||||
{
|
||||
if (Period == null || HourHeight == 0d) return finalSize;
|
||||
|
||||
// Measure/arrange each child height and width.
|
||||
// This is a vertical calendar. Therefore the height of each child is the duration of the event.
|
||||
// Children weights for left and right will be saved if they don't exist.
|
||||
// This is important because we don't want to measure the weights again.
|
||||
// They don't change until new event is added or removed.
|
||||
// Width of the each child may depend on the rectangle packing algorithm.
|
||||
// Children are first categorized into columns. Then each column is shifted to the left until
|
||||
// no overlap occurs. The width of each child is calculated based on the number of columns it spans.
|
||||
|
||||
double availableHeight = finalSize.Height;
|
||||
double availableWidth = finalSize.Width;
|
||||
|
||||
var calendarControls = Children.Cast<ContentPresenter>();
|
||||
|
||||
if (!calendarControls.Any()) return base.ArrangeOverride(finalSize);
|
||||
|
||||
var events = calendarControls.Select(a => a.Content as CalendarItemViewModel).OfType<ICalendarItem>();
|
||||
|
||||
LayoutEvents(events);
|
||||
|
||||
foreach (var control in calendarControls)
|
||||
{
|
||||
// We can't arrange this child.
|
||||
if (!(control.Content is ICalendarItem child)) continue;
|
||||
|
||||
bool isHorizontallyLastItem = false;
|
||||
|
||||
double childWidth = 0,
|
||||
childHeight = Math.Max(0, GetChildHeight(child)),
|
||||
childTop = Math.Max(0, GetChildTopMargin(child, availableHeight)),
|
||||
childLeft = 0;
|
||||
|
||||
// No need to measure anything here.
|
||||
if (childHeight == 0) continue;
|
||||
|
||||
if (!_measurements.ContainsKey(child))
|
||||
{
|
||||
// Multi-day event.
|
||||
|
||||
childLeft = 0;
|
||||
childWidth = availableWidth;
|
||||
}
|
||||
else
|
||||
{
|
||||
var childMeasurement = _measurements[child];
|
||||
|
||||
childWidth = Math.Max(0, GetChildWidth(childMeasurement, finalSize.Width));
|
||||
childLeft = Math.Max(0, GetChildLeftMargin(childMeasurement, availableWidth));
|
||||
|
||||
isHorizontallyLastItem = childMeasurement.Right == 1;
|
||||
}
|
||||
|
||||
// Add additional right margin to items that falls on the right edge of the panel.
|
||||
double extraRightMargin = 0;
|
||||
|
||||
// Multi-day events don't have any margin and their hit test is disabled.
|
||||
if (!child.IsMultiDayEvent)
|
||||
{
|
||||
// Max of 5% of the width or 20px max.
|
||||
extraRightMargin = isHorizontallyLastItem ? Math.Max(LastItemRightExtraMargin, finalSize.Width * 5 / 100) : 0;
|
||||
}
|
||||
|
||||
if (childWidth < 0) childWidth = 1;
|
||||
|
||||
// Regular events must have 2px margin
|
||||
if (!child.IsMultiDayEvent && !child.IsAllDayEvent)
|
||||
{
|
||||
childLeft += 2;
|
||||
childTop += 2;
|
||||
childHeight -= 2;
|
||||
childWidth -= 2;
|
||||
}
|
||||
|
||||
var arrangementRect = new Rect(childLeft + EventItemMargin.Left, childTop + EventItemMargin.Top, Math.Max(childWidth - extraRightMargin, 1), childHeight);
|
||||
|
||||
// Make sure measured size will fit in the arranged box.
|
||||
var measureSize = arrangementRect.ToSize();
|
||||
control.Measure(measureSize);
|
||||
control.Arrange(arrangementRect);
|
||||
|
||||
//Debug.WriteLine($"{child.Title}, Measured: {measureSize}, Arranged: {arrangementRect}");
|
||||
}
|
||||
|
||||
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
#region ColumSpanning and Packing Algorithm
|
||||
|
||||
private void AddOrUpdateMeasurement(ICalendarItem calendarItem, CalendarItemMeasurement measurement)
|
||||
{
|
||||
if (_measurements.ContainsKey(calendarItem))
|
||||
{
|
||||
_measurements[calendarItem] = measurement;
|
||||
}
|
||||
else
|
||||
{
|
||||
_measurements.Add(calendarItem, measurement);
|
||||
}
|
||||
}
|
||||
|
||||
// Pick the left and right positions of each event, such that there are no overlap.
|
||||
private void LayoutEvents(IEnumerable<ICalendarItem> events)
|
||||
{
|
||||
var columns = new List<List<ICalendarItem>>();
|
||||
DateTime? lastEventEnding = null;
|
||||
|
||||
foreach (var ev in events.OrderBy(ev => ev.StartDate).ThenBy(ev => ev.EndDate))
|
||||
{
|
||||
// Multi-day events are not measured.
|
||||
if (ev.IsMultiDayEvent) continue;
|
||||
|
||||
if (ev.Period.Start >= lastEventEnding)
|
||||
{
|
||||
PackEvents(columns);
|
||||
columns.Clear();
|
||||
lastEventEnding = null;
|
||||
}
|
||||
|
||||
bool placed = false;
|
||||
|
||||
foreach (var col in columns)
|
||||
{
|
||||
if (!col.Last().Period.OverlapsWith(ev.Period))
|
||||
{
|
||||
col.Add(ev);
|
||||
placed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!placed)
|
||||
{
|
||||
columns.Add(new List<ICalendarItem> { ev });
|
||||
}
|
||||
if (lastEventEnding == null || ev.Period.End > lastEventEnding.Value)
|
||||
{
|
||||
lastEventEnding = ev.Period.End;
|
||||
}
|
||||
}
|
||||
if (columns.Count > 0)
|
||||
{
|
||||
PackEvents(columns);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the left and right positions for each event in the connected group.
|
||||
private void PackEvents(List<List<ICalendarItem>> columns)
|
||||
{
|
||||
float numColumns = columns.Count;
|
||||
int iColumn = 0;
|
||||
|
||||
foreach (var col in columns)
|
||||
{
|
||||
foreach (var ev in col)
|
||||
{
|
||||
int colSpan = ExpandEvent(ev, iColumn, columns);
|
||||
|
||||
var leftWeight = iColumn / numColumns;
|
||||
var rightWeight = (iColumn + colSpan) / numColumns;
|
||||
|
||||
AddOrUpdateMeasurement(ev, new CalendarItemMeasurement(leftWeight, rightWeight));
|
||||
}
|
||||
|
||||
iColumn++;
|
||||
}
|
||||
}
|
||||
|
||||
// Checks how many columns the event can expand into, without colliding with other events.
|
||||
private int ExpandEvent(ICalendarItem ev, int iColumn, List<List<ICalendarItem>> columns)
|
||||
{
|
||||
int colSpan = 1;
|
||||
|
||||
foreach (var col in columns.Skip(iColumn + 1))
|
||||
{
|
||||
foreach (var ev1 in col)
|
||||
{
|
||||
if (ev1.Period.OverlapsWith(ev.Period)) return colSpan;
|
||||
}
|
||||
|
||||
colSpan++;
|
||||
}
|
||||
|
||||
return colSpan;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Input;
|
||||
using CommunityToolkit.Diagnostics;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Mail.WinUI.Controls;
|
||||
|
||||
namespace Wino.Calendar.Controls;
|
||||
|
||||
@@ -11,6 +12,7 @@ public partial class WinoCalendarTypeSelectorControl : Control
|
||||
private const string PART_TodayButton = nameof(PART_TodayButton);
|
||||
private const string PART_DayToggle = nameof(PART_DayToggle);
|
||||
private const string PART_WeekToggle = nameof(PART_WeekToggle);
|
||||
private const string PART_WorkWeekToggle = nameof(PART_WorkWeekToggle);
|
||||
private const string PART_MonthToggle = nameof(PART_MonthToggle);
|
||||
|
||||
public static readonly DependencyProperty SelectedTypeProperty = DependencyProperty.Register(
|
||||
@@ -42,6 +44,7 @@ public partial class WinoCalendarTypeSelectorControl : Control
|
||||
private AppBarButton? _todayButton;
|
||||
private AppBarToggleButton? _dayToggle;
|
||||
private AppBarToggleButton? _weekToggle;
|
||||
private AppBarToggleButton? _workWeekToggle;
|
||||
private AppBarToggleButton? _monthToggle;
|
||||
|
||||
public WinoCalendarTypeSelectorControl()
|
||||
@@ -58,24 +61,34 @@ public partial class WinoCalendarTypeSelectorControl : Control
|
||||
_todayButton = GetTemplateChild(PART_TodayButton) as AppBarButton;
|
||||
_dayToggle = GetTemplateChild(PART_DayToggle) as AppBarToggleButton;
|
||||
_weekToggle = GetTemplateChild(PART_WeekToggle) as AppBarToggleButton;
|
||||
_workWeekToggle = GetTemplateChild(PART_WorkWeekToggle) as AppBarToggleButton;
|
||||
_monthToggle = GetTemplateChild(PART_MonthToggle) as AppBarToggleButton;
|
||||
|
||||
Guard.IsNotNull(_todayButton, nameof(_todayButton));
|
||||
Guard.IsNotNull(_dayToggle, nameof(_dayToggle));
|
||||
Guard.IsNotNull(_weekToggle, nameof(_weekToggle));
|
||||
Guard.IsNotNull(_workWeekToggle, nameof(_workWeekToggle));
|
||||
Guard.IsNotNull(_monthToggle, nameof(_monthToggle));
|
||||
|
||||
_todayButton!.Click += TodayClicked;
|
||||
|
||||
_dayToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Day); };
|
||||
_weekToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Week); };
|
||||
_monthToggle!.Click += (s, e) => { SetSelectedType(CalendarDisplayType.Month); };
|
||||
_dayToggle!.Click += DayToggleClicked;
|
||||
_weekToggle!.Click += WeekToggleClicked;
|
||||
_workWeekToggle!.Click += WorkWeekToggleClicked;
|
||||
_monthToggle!.Click += MonthToggleClicked;
|
||||
|
||||
UpdateToggleButtonStates();
|
||||
}
|
||||
|
||||
private void TodayClicked(object? sender, RoutedEventArgs e) => TodayClickedCommand?.Execute(null);
|
||||
|
||||
private void DayToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Day);
|
||||
|
||||
private void WeekToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Week);
|
||||
|
||||
private void WorkWeekToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.WorkWeek);
|
||||
|
||||
private void MonthToggleClicked(object sender, RoutedEventArgs e) => SetSelectedType(CalendarDisplayType.Month);
|
||||
|
||||
private void SetSelectedType(CalendarDisplayType type)
|
||||
{
|
||||
SelectedType = type;
|
||||
@@ -84,8 +97,10 @@ public partial class WinoCalendarTypeSelectorControl : Control
|
||||
|
||||
private static void OnSelectedTypeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var control = d as WinoCalendarTypeSelectorControl;
|
||||
control?.UpdateToggleButtonStates();
|
||||
if (d is WinoCalendarTypeSelectorControl control)
|
||||
{
|
||||
control.UpdateToggleButtonStates();
|
||||
}
|
||||
}
|
||||
|
||||
private void UnregisterHandlers()
|
||||
@@ -94,17 +109,38 @@ public partial class WinoCalendarTypeSelectorControl : Control
|
||||
{
|
||||
_todayButton.Click -= TodayClicked;
|
||||
}
|
||||
|
||||
if (_dayToggle != null)
|
||||
{
|
||||
_dayToggle.Click -= DayToggleClicked;
|
||||
}
|
||||
|
||||
if (_weekToggle != null)
|
||||
{
|
||||
_weekToggle.Click -= WeekToggleClicked;
|
||||
}
|
||||
|
||||
if (_workWeekToggle != null)
|
||||
{
|
||||
_workWeekToggle.Click -= WorkWeekToggleClicked;
|
||||
}
|
||||
|
||||
if (_monthToggle != null)
|
||||
{
|
||||
_monthToggle.Click -= MonthToggleClicked;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateToggleButtonStates()
|
||||
{
|
||||
if (_dayToggle == null || _weekToggle == null || _monthToggle == null)
|
||||
if (_dayToggle == null || _weekToggle == null || _workWeekToggle == null || _monthToggle == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_dayToggle.IsChecked = SelectedType == CalendarDisplayType.Day;
|
||||
_weekToggle.IsChecked = SelectedType == CalendarDisplayType.Week;
|
||||
_workWeekToggle.IsChecked = SelectedType == CalendarDisplayType.WorkWeek;
|
||||
_monthToggle.IsChecked = SelectedType == CalendarDisplayType.Month;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +76,7 @@ public static class ControlConstants
|
||||
{ WinoIconGlyph.CalendarToday, "\uE911" },
|
||||
{ WinoIconGlyph.CalendarDay, "\uE913" },
|
||||
{ WinoIconGlyph.CalendarWeek, "\uE914" },
|
||||
{ WinoIconGlyph.CalendarWorkWeek, "\uE914" },
|
||||
{ WinoIconGlyph.CalendarMonth, "\uE91c" },
|
||||
{ WinoIconGlyph.CalendarYear, "\uE917" },
|
||||
{ WinoIconGlyph.WeatherBlow, "\uE907" },
|
||||
|
||||
@@ -43,6 +43,7 @@ public static class XamlHelpers
|
||||
public static Visibility ReverseBoolToVisibilityConverter(bool value) => value ? Visibility.Collapsed : Visibility.Visible;
|
||||
public static Visibility ReverseVisibilityConverter(Visibility visibility) => visibility == Visibility.Visible ? Visibility.Collapsed : Visibility.Visible;
|
||||
public static bool ReverseBoolConverter(bool value) => !value;
|
||||
public static GridLength DoubleToGridLength(double value) => new(value);
|
||||
public static bool AreEqual(int value1, int value2) => value1 == value2;
|
||||
public static bool ShouldDisplayPreview(string text) => text == null ? false : text.Any(x => char.IsLetter(x));
|
||||
public static bool CountToBooleanConverter(int value) => value > 0;
|
||||
|
||||
@@ -311,6 +311,12 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
|
||||
set => SaveProperty(propertyName: nameof(HourHeight), value);
|
||||
}
|
||||
|
||||
public string CalendarTimedDayHeaderDateFormat
|
||||
{
|
||||
get => _configurationService.Get(nameof(CalendarTimedDayHeaderDateFormat), "ddd dd");
|
||||
set => SaveProperty(propertyName: nameof(CalendarTimedDayHeaderDateFormat), string.IsNullOrWhiteSpace(value) ? "ddd dd" : value.Trim());
|
||||
}
|
||||
|
||||
public TimeSpan WorkingHourStart
|
||||
{
|
||||
get => _configurationService.Get(nameof(WorkingHourStart), new TimeSpan(8, 0, 0));
|
||||
@@ -402,7 +408,8 @@ public class PreferencesService(IConfigurationService configurationService) : Ob
|
||||
WorkingHourEnd,
|
||||
HourHeight,
|
||||
Prefer24HourTimeFormat ? DayHeaderDisplayType.TwentyFourHour : DayHeaderDisplayType.TwelveHour,
|
||||
new CultureInfo(WinoTranslationDictionary.GetLanguageFileNameRelativePath(CurrentLanguage)));
|
||||
new CultureInfo(WinoTranslationDictionary.GetLanguageFileNameRelativePath(CurrentLanguage)),
|
||||
CalendarTimedDayHeaderDateFormat);
|
||||
}
|
||||
|
||||
private List<DayOfWeek> GetDaysBetween(DayOfWeek startDay, DayOfWeek endDay)
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Wino.Calendar.Controls">
|
||||
|
||||
<!-- Left day header DayHeaderControl -->
|
||||
<Style TargetType="controls:DayHeaderControl">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="controls:DayHeaderControl">
|
||||
<Grid>
|
||||
<TextBlock
|
||||
x:Name="PART_DayHeaderTextBlock"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Top"
|
||||
FontSize="12" />
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -2,8 +2,7 @@
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="using:Wino.Calendar.Controls"
|
||||
xmlns:controls1="using:Wino.Mail.WinUI.Controls"
|
||||
xmlns:muxc="using:Microsoft.UI.Xaml.Controls">
|
||||
xmlns:controls1="using:Wino.Mail.WinUI.Controls">
|
||||
|
||||
<Style TargetType="controls:WinoCalendarTypeSelectorControl">
|
||||
<Style.Setters>
|
||||
@@ -16,7 +15,6 @@
|
||||
DefaultLabelPosition="Right">
|
||||
|
||||
<CommandBar.PrimaryCommands>
|
||||
<!-- Today -->
|
||||
<AppBarButton
|
||||
x:Name="PART_TodayButton"
|
||||
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
|
||||
@@ -28,8 +26,6 @@
|
||||
|
||||
<AppBarSeparator />
|
||||
|
||||
<!-- Day -->
|
||||
<!-- TODO: Specific days -->
|
||||
<AppBarToggleButton
|
||||
x:Name="PART_DayToggle"
|
||||
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
|
||||
@@ -39,9 +35,6 @@
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
|
||||
<!-- Week -->
|
||||
<!-- TODO: Work week -->
|
||||
|
||||
<AppBarToggleButton
|
||||
x:Name="PART_WeekToggle"
|
||||
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
|
||||
@@ -51,7 +44,15 @@
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
|
||||
<!-- Month -->
|
||||
<AppBarToggleButton
|
||||
x:Name="PART_WorkWeekToggle"
|
||||
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
|
||||
Label="Work Week">
|
||||
<AppBarToggleButton.Icon>
|
||||
<controls1:WinoFontIcon Icon="CalendarWorkWeek" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
|
||||
<AppBarToggleButton
|
||||
x:Name="PART_MonthToggle"
|
||||
Foreground="{ThemeResource ApplicationForegroundThemeBrush}"
|
||||
@@ -60,14 +61,11 @@
|
||||
<controls1:WinoFontIcon FontSize="44" Icon="CalendarMonth" />
|
||||
</AppBarToggleButton.Icon>
|
||||
</AppBarToggleButton>
|
||||
|
||||
</CommandBar.PrimaryCommands>
|
||||
</CommandBar>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style.Setters>
|
||||
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
|
||||
|
||||
@@ -4,16 +4,224 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:abstract="using:Wino.Calendar.Views.Abstract"
|
||||
xmlns:calendarControls="using:Wino.Calendar.Controls"
|
||||
xmlns:collections="using:CommunityToolkit.Mvvm.Collections"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:data="using:Wino.Calendar.ViewModels.Data"
|
||||
xmlns:domain="using:Wino.Core.Domain"
|
||||
xmlns:helpers="using:Wino.Helpers"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d">
|
||||
|
||||
<Page.Resources>
|
||||
<CollectionViewSource
|
||||
x:Name="GroupedCalendarEnumerableViewSource"
|
||||
IsSourceGrouped="True"
|
||||
Source="{x:Bind ViewModel.AccountCalendarStateService.GroupedCalendars, Mode=OneWay}" />
|
||||
</Page.Resources>
|
||||
|
||||
<Grid>
|
||||
<Border
|
||||
Margin="4,0,7,7"
|
||||
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="7">
|
||||
<calendarControls:CalendarPeriodControl
|
||||
x:Name="CalendarSurface"
|
||||
IsEnabled="{x:Bind ViewModel.IsCalendarEnabled, Mode=OneWay}"
|
||||
CalendarItems="{x:Bind ViewModel.CalendarItems, Mode=OneWay}"
|
||||
CalendarSettings="{x:Bind ViewModel.CurrentSettings, Mode=OneWay}"
|
||||
EmptySlotTapped="CalendarSurfaceEmptySlotTapped"
|
||||
IsEnabled="{x:Bind ViewModel.IsCalendarEnabled, Mode=OneWay}"
|
||||
VisibleRange="{x:Bind ViewModel.CurrentVisibleRange, Mode=OneWay}" />
|
||||
</Border>
|
||||
|
||||
<Canvas x:Name="CalendarOverlayCanvas" IsHitTestVisible="False">
|
||||
<Grid
|
||||
x:Name="TeachingTipPositionerGrid"
|
||||
Background="Transparent"
|
||||
IsHitTestVisible="False"
|
||||
Visibility="Visible" />
|
||||
|
||||
<Popup
|
||||
x:Name="QuickEventPopupDialog"
|
||||
ActualPlacementChanged="PopupPlacementChanged"
|
||||
Closed="QuickEventPopupClosed"
|
||||
DesiredPlacement="{x:Bind helpers:XamlHelpers.GetPlaccementModeForCalendarType(ViewModel.StatePersistanceService.CalendarDisplayType), Mode=OneWay}"
|
||||
IsLightDismissEnabled="True"
|
||||
IsOpen="{x:Bind ViewModel.IsQuickEventDialogOpen, Mode=TwoWay}"
|
||||
PlacementTarget="{x:Bind TeachingTipPositionerGrid}">
|
||||
<Popup.ChildTransitions>
|
||||
<TransitionCollection>
|
||||
<PopupThemeTransition />
|
||||
</TransitionCollection>
|
||||
</Popup.ChildTransitions>
|
||||
|
||||
<Grid
|
||||
MinWidth="440"
|
||||
MaxWidth="520"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
BorderBrush="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(ViewModel.SelectedQuickEventAccountCalendar.BackgroundColorHex), Mode=OneWay, TargetNullValue='LightGray'}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="8"
|
||||
RowSpacing="12">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Button
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
Background="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(ViewModel.SelectedQuickEventAccountCalendar.BackgroundColorHex), Mode=OneWay, TargetNullValue='LightGray'}"
|
||||
CornerRadius="8,8,0,0">
|
||||
<Button.Content>
|
||||
<Grid
|
||||
Height="36"
|
||||
Padding="12,0"
|
||||
ColumnSpacing="10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(ViewModel.SelectedQuickEventAccountCalendar.BackgroundColorHex), Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.SelectedQuickEventAccountCalendar.Account.Name, Mode=OneWay}" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(ViewModel.SelectedQuickEventAccountCalendar.BackgroundColorHex), Mode=OneWay}"
|
||||
Text="{x:Bind ViewModel.SelectedQuickEventAccountCalendarName, Mode=OneWay}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<FontIcon
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
Foreground="{x:Bind helpers:XamlHelpers.GetReadableTextColor(ViewModel.SelectedQuickEventAccountCalendar.BackgroundColorHex), Mode=OneWay}"
|
||||
Glyph="" />
|
||||
</Grid>
|
||||
</Button.Content>
|
||||
<Button.Flyout>
|
||||
<Flyout x:Name="QuickEventAccountSelectorFlyout" Placement="Bottom">
|
||||
<ListView
|
||||
MaxHeight="300"
|
||||
ItemsSource="{x:Bind GroupedCalendarEnumerableViewSource.View, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedQuickEventAccountCalendar, Mode=TwoWay}"
|
||||
SelectionChanged="QuickEventAccountSelectorSelectionChanged">
|
||||
<ListView.ItemTemplate>
|
||||
<DataTemplate x:DataType="data:AccountCalendarViewModel">
|
||||
<StackPanel
|
||||
Margin="0,0,16,0"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8">
|
||||
<Ellipse
|
||||
Width="14"
|
||||
Height="14"
|
||||
Fill="{x:Bind helpers:XamlHelpers.GetSolidColorBrushFromHex(BackgroundColorHex), Mode=OneWay}" />
|
||||
<TextBlock VerticalAlignment="Center" Text="{x:Bind Name, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListView.ItemTemplate>
|
||||
<ListView.GroupStyle>
|
||||
<GroupStyle>
|
||||
<GroupStyle.HeaderTemplate>
|
||||
<DataTemplate x:DataType="collections:IReadOnlyObservableGroup">
|
||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind Key.ToString()}" />
|
||||
</DataTemplate>
|
||||
</GroupStyle.HeaderTemplate>
|
||||
</GroupStyle>
|
||||
</ListView.GroupStyle>
|
||||
</ListView>
|
||||
</Flyout>
|
||||
</Button.Flyout>
|
||||
</Button>
|
||||
|
||||
<Grid
|
||||
Grid.Row="1"
|
||||
Padding="12"
|
||||
RowSpacing="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBox
|
||||
FontSize="16"
|
||||
PlaceholderText="{x:Bind domain:Translator.QuickEventDialog_EventName}"
|
||||
Text="{x:Bind ViewModel.EventName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<CheckBox
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Content="{x:Bind domain:Translator.QuickEventDialog_IsAllDay}"
|
||||
IsChecked="{x:Bind ViewModel.IsAllDay, Mode=TwoWay}" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1" ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ComboBox
|
||||
IsEditable="True"
|
||||
IsEnabled="{x:Bind helpers:XamlHelpers.ReverseBoolConverter(ViewModel.IsAllDay), Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.HourSelectionStrings, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedStartTimeString, Mode=TwoWay}"
|
||||
TextSubmitted="StartTimeDurationSubmitted" />
|
||||
|
||||
<TextBlock
|
||||
Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
Text="-" />
|
||||
|
||||
<ComboBox
|
||||
Grid.Column="2"
|
||||
IsEditable="True"
|
||||
IsEnabled="{x:Bind helpers:XamlHelpers.ReverseBoolConverter(ViewModel.IsAllDay), Mode=OneWay}"
|
||||
ItemsSource="{x:Bind ViewModel.HourSelectionStrings, Mode=OneWay}"
|
||||
SelectedItem="{x:Bind ViewModel.SelectedEndTimeString, Mode=TwoWay}"
|
||||
TextSubmitted="EndTimeDurationSubmitted" />
|
||||
</Grid>
|
||||
|
||||
<TextBox
|
||||
Grid.Row="2"
|
||||
PlaceholderText="{x:Bind domain:Translator.QuickEventDialog_Location}"
|
||||
Text="{x:Bind ViewModel.Location, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
|
||||
<Grid Grid.Row="3" ColumnSpacing="12">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Command="{x:Bind ViewModel.MoreDetailsCommand}" -->
|
||||
<Button HorizontalAlignment="Stretch" Content="{x:Bind domain:Translator.QuickEventDialogMoreDetailsButtonText}" />
|
||||
|
||||
<Button
|
||||
Grid.Column="1"
|
||||
HorizontalAlignment="Stretch"
|
||||
Command="{x:Bind ViewModel.SaveQuickEventCommand}"
|
||||
Content="{x:Bind domain:Translator.Buttons_Save}"
|
||||
Style="{ThemeResource AccentButtonStyle}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Popup>
|
||||
</Canvas>
|
||||
</Grid>
|
||||
</abstract:CalendarPageAbstract>
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using System;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Controls.Primitives;
|
||||
using Microsoft.UI.Xaml.Navigation;
|
||||
using Windows.Foundation;
|
||||
using Wino.Calendar.Controls;
|
||||
using Wino.Calendar.Views.Abstract;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Messaging.Client.Calendar;
|
||||
@@ -9,6 +14,8 @@ namespace Wino.Calendar.Views;
|
||||
|
||||
public sealed partial class CalendarPage : CalendarPageAbstract
|
||||
{
|
||||
private const int PopupDialogOffset = 12;
|
||||
|
||||
public CalendarPage()
|
||||
{
|
||||
InitializeComponent();
|
||||
@@ -38,4 +45,70 @@ public sealed partial class CalendarPage : CalendarPageAbstract
|
||||
var request = new CalendarDisplayRequest(ViewModel.StatePersistanceService.CalendarDisplayType, anchorDate);
|
||||
WeakReferenceMessenger.Default.Send(new LoadCalendarMessage(request));
|
||||
}
|
||||
|
||||
private void CalendarSurfaceEmptySlotTapped(object sender, CalendarEmptySlotTappedEventArgs e)
|
||||
{
|
||||
if (ViewModel.DisplayDetailsCalendarItemViewModel != null)
|
||||
{
|
||||
ViewModel.DisplayDetailsCalendarItemViewModel = null;
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.SelectedQuickEventDate = e.ClickedDate;
|
||||
|
||||
var transform = CalendarSurface.TransformToVisual(CalendarOverlayCanvas);
|
||||
var canvasPoint = transform.TransformPoint(e.PositionerPoint);
|
||||
|
||||
TeachingTipPositionerGrid.Width = e.CellSize.Width;
|
||||
TeachingTipPositionerGrid.Height = e.CellSize.Height;
|
||||
|
||||
Canvas.SetLeft(TeachingTipPositionerGrid, canvasPoint.X);
|
||||
Canvas.SetTop(TeachingTipPositionerGrid, canvasPoint.Y);
|
||||
|
||||
var startTime = e.ClickedDate.TimeOfDay;
|
||||
var endTime = startTime.Add(TimeSpan.FromMinutes(30));
|
||||
ViewModel.SelectQuickEventTimeRange(startTime, endTime);
|
||||
|
||||
QuickEventPopupDialog.IsOpen = true;
|
||||
}
|
||||
|
||||
private void QuickEventAccountSelectorSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||
=> QuickEventAccountSelectorFlyout.Hide();
|
||||
|
||||
private void QuickEventPopupClosed(object sender, object e)
|
||||
{
|
||||
}
|
||||
|
||||
private void PopupPlacementChanged(object sender, object e)
|
||||
{
|
||||
if (sender is not Popup popup)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
popup.HorizontalOffset = 0;
|
||||
popup.VerticalOffset = 0;
|
||||
|
||||
switch (popup.ActualPlacement)
|
||||
{
|
||||
case PopupPlacementMode.Top:
|
||||
popup.VerticalOffset = PopupDialogOffset * -1;
|
||||
break;
|
||||
case PopupPlacementMode.Bottom:
|
||||
popup.VerticalOffset = PopupDialogOffset;
|
||||
break;
|
||||
case PopupPlacementMode.Left:
|
||||
popup.HorizontalOffset = PopupDialogOffset * -1;
|
||||
break;
|
||||
case PopupPlacementMode.Right:
|
||||
popup.HorizontalOffset = PopupDialogOffset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
|
||||
=> ViewModel.SelectedStartTimeString = args.Text;
|
||||
|
||||
private void EndTimeDurationSubmitted(ComboBox sender, ComboBoxTextSubmittedEventArgs args)
|
||||
=> ViewModel.SelectedEndTimeString = args.Text;
|
||||
}
|
||||
|
||||
@@ -175,10 +175,36 @@
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<PathIcon Data="F1 M 4.921875 18.75 C 4.433594 18.75 3.966471 18.650717 3.520508 18.452148 C 3.074544 18.25358 2.683919 17.986654 2.348633 17.651367 C 2.013346 17.31608 1.746419 16.925455 1.547852 16.479492 C 1.349284 16.033529 1.25 15.566406 1.25 15.078125 L 1.25 4.921875 C 1.25 4.433594 1.349284 3.966473 1.547852 3.520508 C 1.746419 3.074545 2.013346 2.68392 2.348633 2.348633 C 2.683919 2.013348 3.074544 1.74642 3.520508 1.547852 C 3.966471 1.349285 4.433594 1.25 4.921875 1.25 L 15.078125 1.25 C 15.566406 1.25 16.033527 1.349285 16.479492 1.547852 C 16.925455 1.74642 17.31608 2.013348 17.651367 2.348633 C 17.986652 2.68392 18.25358 3.074545 18.452148 3.520508 C 18.650715 3.966473 18.75 4.433594 18.75 4.921875 L 18.75 15.078125 C 18.75 15.566406 18.650715 16.033529 18.452148 16.479492 C 18.25358 16.925455 17.986652 17.31608 17.651367 17.651367 C 17.31608 17.986654 16.925455 18.25358 16.479492 18.452148 C 16.033527 18.650717 15.566406 18.75 15.078125 18.75 Z M 2.5 10 L 17.5 10 L 17.5 4.951172 C 17.5 4.625651 17.433268 4.314779 17.299805 4.018555 C 17.16634 3.722332 16.987305 3.461914 16.762695 3.237305 C 16.538086 3.012695 16.277668 2.83366 15.981445 2.700195 C 15.685221 2.566732 15.374349 2.5 15.048828 2.5 L 4.951172 2.5 C 4.625651 2.5 4.314778 2.566732 4.018555 2.700195 C 3.722331 2.83366 3.461914 3.012695 3.237305 3.237305 C 3.012695 3.461914 2.833659 3.722332 2.700195 4.018555 C 2.566732 4.314779 2.5 4.625651 2.5 4.951172 Z " />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<StackPanel Spacing="8">
|
||||
<ToggleSwitch
|
||||
IsOn="{x:Bind ViewModel.Is24HourHeaders, Mode=TwoWay}"
|
||||
OffContent="12h"
|
||||
OnContent="24h" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.TimedHourLabelPreview, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
|
||||
<controls:SettingsCard
|
||||
Description="{x:Bind domain:Translator.CalendarSettings_TimedDayHeaderFormat_Description}"
|
||||
Header="{x:Bind domain:Translator.CalendarSettings_TimedDayHeaderFormat_Header}">
|
||||
<controls:SettingsCard.HeaderIcon>
|
||||
<FontIcon Glyph="" />
|
||||
</controls:SettingsCard.HeaderIcon>
|
||||
<StackPanel Spacing="8">
|
||||
<TextBox
|
||||
PlaceholderText="ddd dd"
|
||||
Text="{x:Bind ViewModel.TimedDayHeaderDateFormat, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
|
||||
<ComboBox
|
||||
ItemsSource="{x:Bind ViewModel.TimedDayHeaderFormatPresets, Mode=OneWay}"
|
||||
SelectedIndex="{x:Bind ViewModel.SelectedTimedDayHeaderFormatPresetIndex, Mode=TwoWay}" />
|
||||
<TextBlock
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Text="{x:Bind ViewModel.TimedDayHeaderFormatPreview, Mode=OneWay}"
|
||||
TextWrapping="WrapWholeWords" />
|
||||
</StackPanel>
|
||||
</controls:SettingsCard>
|
||||
</controls:SettingsExpander.Items>
|
||||
</controls:SettingsExpander>
|
||||
@@ -289,7 +315,7 @@
|
||||
<StateTrigger IsActive="{x:Bind ViewModel.Is24HourHeaders, Mode=OneWay}" />
|
||||
</VisualState.StateTriggers>
|
||||
<VisualState.Setters>
|
||||
<Setter Target="WorkStartStartPicker.ClockIdentifier" Value="24HourClock" />
|
||||
<Setter Target="WorkHourStartPicker.ClockIdentifier" Value="24HourClock" />
|
||||
<Setter Target="WorkEndStartPicker.ClockIdentifier" Value="24HourClock" />
|
||||
</VisualState.Setters>
|
||||
</VisualState>
|
||||
|
||||
@@ -201,7 +201,7 @@ public sealed partial class WinoAppShell : Views.Abstract.WinoAppShellAbstract,
|
||||
|
||||
if (ViewModel.IsCalendarMode)
|
||||
{
|
||||
ViewModel.StatePersistenceService.CoreWindowTitle = ViewModel.CalendarClient.VisibleDateRangeText ?? string.Empty;
|
||||
ViewModel.StatePersistenceService.CoreWindowTitle = string.Empty;
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -120,7 +120,6 @@
|
||||
<None Remove="Styles\CalendarShellNavigationViewStyle.xaml" />
|
||||
<None Remove="Styles\CalendarThemeResources.xaml" />
|
||||
<None Remove="Styles\CalendarViewStyles.xaml" />
|
||||
<None Remove="Styles\DayHeaderControl.xaml" />
|
||||
<None Remove="Styles\WinoCalendarTypeSelectorControl.xaml" />
|
||||
<None Remove="Views\Calendar\CalendarAppShell.xaml" />
|
||||
<None Remove="Views\Calendar\CalendarPage.xaml" />
|
||||
@@ -367,9 +366,6 @@
|
||||
<Page Update="Styles\CalendarThemeResources.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Styles\DayHeaderControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
<Page Update="Styles\WinoCalendarTypeSelectorControl.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
</Page>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user