| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640 |
- using Avalonia;
- using Avalonia.Controls;
- using Avalonia.Controls.Shapes;
- using Avalonia.Controls.Templates;
- using Avalonia.Data;
- using Avalonia.Input;
- using Avalonia.Media;
- using Avalonia.Metadata;
- using DynamicData.Binding;
- using InABox.Core;
- using System.Collections;
- using System.Collections.Specialized;
- using System.Diagnostics.CodeAnalysis;
- using System.Reactive.Linq;
- using System.Runtime.Serialization;
- namespace InABox.Avalonia.Components;
- public class CalendarBlockEventArgs(object? value, object column, TimeSpan start, TimeSpan end) : EventArgs
- {
- public object? Value { get; set; } = value;
- public object Column { get; set; } = column;
- public TimeSpan Start { get; set; } = start;
- public TimeSpan End { get; set; } = end;
- }
- public partial class CalendarView : UserControl
- {
- public static readonly StyledProperty<double> RowHeightProperty =
- AvaloniaProperty.Register<CalendarView, double>(nameof(RowHeight), defaultValue: 100);
- public static readonly StyledProperty<double> MinimumColumnWidthProperty =
- AvaloniaProperty.Register<CalendarView, double>(nameof(MinimumColumnWidth), defaultValue: 50);
- public static readonly StyledProperty<TimeSpan> RowIntervalProperty =
- AvaloniaProperty.Register<CalendarView, TimeSpan>(nameof(RowInterval), defaultValue: TimeSpan.FromHours(1));
- public static readonly StyledProperty<IBinding?> ColumnMappingProperty =
- AvaloniaProperty.Register<CalendarView, IBinding?>(nameof(ColumnMapping));
- public static readonly StyledProperty<IBinding?> StartTimeMappingProperty =
- AvaloniaProperty.Register<CalendarView, IBinding?>(nameof(StartTimeMapping));
- public static readonly StyledProperty<IBinding?> EndTimeMappingProperty =
- AvaloniaProperty.Register<CalendarView, IBinding?>(nameof(EndTimeMapping));
- public static readonly StyledProperty<IEnumerable?> ItemsSourceProperty =
- AvaloniaProperty.Register<CalendarView, IEnumerable?>(nameof(ItemsSource));
- public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
- AvaloniaProperty.Register<CalendarView, IDataTemplate?>(nameof(ItemTemplate));
- public static readonly StyledProperty<IDataTemplate?> HeaderTemplateProperty =
- AvaloniaProperty.Register<CalendarView, IDataTemplate?>(nameof(HeaderTemplate));
- public static readonly StyledProperty<IEnumerable?> ColumnsProperty =
- AvaloniaProperty.Register<CalendarView, IEnumerable?>(nameof(Columns));
- public static readonly StyledProperty<bool> ShowColumnsProperty =
- AvaloniaProperty.Register<CalendarView, bool>(nameof(ShowColumns));
- public double MinimumColumnWidth
- {
- get => GetValue(MinimumColumnWidthProperty);
- set => SetValue(MinimumColumnWidthProperty, value);
- }
- public double RowHeight
- {
- get => GetValue(RowHeightProperty);
- set => SetValue(RowHeightProperty, value);
- }
- public TimeSpan RowInterval
- {
- get => GetValue(RowIntervalProperty);
- set => SetValue(RowIntervalProperty, value);
- }
- [AssignBinding]
- [InheritDataTypeFromItems(nameof(ItemsSource))]
- public IBinding? ColumnMapping
- {
- get => GetValue(ColumnMappingProperty);
- set => SetValue(ColumnMappingProperty, value);
- }
- [AssignBinding]
- [InheritDataTypeFromItems(nameof(ItemsSource))]
- public IBinding? StartTimeMapping
- {
- get => GetValue(StartTimeMappingProperty);
- set => SetValue(StartTimeMappingProperty, value);
- }
- [AssignBinding]
- [InheritDataTypeFromItems(nameof(ItemsSource))]
- public IBinding? EndTimeMapping
- {
- get => GetValue(EndTimeMappingProperty);
- set => SetValue(EndTimeMappingProperty, value);
- }
- public IEnumerable? ItemsSource
- {
- get => GetValue(ItemsSourceProperty);
- set => SetValue(ItemsSourceProperty, value);
- }
- [InheritDataTypeFromItems(nameof(ItemsSource))]
- public IDataTemplate? ItemTemplate
- {
- get => GetValue(ItemTemplateProperty);
- set => SetValue(ItemTemplateProperty, value);
- }
- public IDataTemplate? HeaderTemplate
- {
- get => GetValue(HeaderTemplateProperty);
- set => SetValue(HeaderTemplateProperty, value);
- }
- public IEnumerable? Columns
- {
- get => GetValue(ColumnsProperty);
- set => SetValue(ColumnsProperty, value);
- }
- public bool ShowColumns
- {
- get => GetValue(ShowColumnsProperty);
- set => SetValue(ShowColumnsProperty, value);
- }
- public event EventHandler<CalendarBlockEventArgs>? BlockClicked;
- public event EventHandler<CalendarBlockEventArgs>? BlockHeld;
- static CalendarView()
- {
- ItemsSourceProperty.Changed.AddClassHandler<CalendarView>(ItemsSource_Changed);
- RowHeightProperty.Changed.AddClassHandler<CalendarView>(Render_Changed);
- MinimumColumnWidthProperty.Changed.AddClassHandler<CalendarView>(Render_Changed);
- RowIntervalProperty.Changed.AddClassHandler<CalendarView>(Render_Changed);
- ColumnsProperty.Changed.AddClassHandler<CalendarView>(Columns_Changed);
- }
- private static void Columns_Changed(CalendarView view, AvaloniaPropertyChangedEventArgs args)
- {
- if(args.OldValue is INotifyCollectionChanged oldNotify)
- {
- oldNotify.CollectionChanged -= view.ColumnsCollection_Changed;
- }
- view.Render(itemsChanged: true);
- if(args.NewValue is INotifyCollectionChanged notify)
- {
- notify.CollectionChanged += view.ColumnsCollection_Changed;
- }
- }
- private void ColumnsCollection_Changed(object? sender, NotifyCollectionChangedEventArgs e)
- {
- Render(itemsChanged: true);
- }
- private static void Render_Changed(CalendarView view, AvaloniaPropertyChangedEventArgs args)
- {
- view.Render();
- }
- public CalendarView()
- {
- InitializeComponent();
- ScrollViewer.GetPropertyChangedObservable(ScrollViewer.OffsetProperty).Subscribe(x =>
- {
- LabelScroll.Offset = new(0, ScrollViewer.Offset.Y);
- HeaderScroll.Offset = new(ScrollViewer.Offset.X, 0);
- });
- }
- private static void ItemsSource_Changed(CalendarView view, AvaloniaPropertyChangedEventArgs args)
- {
- if(args.OldValue is INotifyCollectionChanged oldNotify)
- {
- oldNotify.CollectionChanged -= view.Collection_Changed;
- }
- view.Render(itemsChanged: true);
- if(view.ItemsSource is INotifyCollectionChanged notify)
- {
- notify.CollectionChanged += view.Collection_Changed;
- }
- }
- private void Collection_Changed(object? sender, NotifyCollectionChangedEventArgs e)
- {
- Render(itemsChanged: true);
- }
- private void Scroll_SizeChanged(object? sender, SizeChangedEventArgs e)
- {
- Render();
- }
- private class Block : ContentControl
- {
- public static readonly StyledProperty<object> ColumnProperty =
- AvaloniaProperty.Register<Block, object>(nameof(Column));
- public static readonly StyledProperty<TimeSpan> StartTimeProperty =
- AvaloniaProperty.Register<Block, TimeSpan>(nameof(StartTime));
- public static readonly StyledProperty<TimeSpan> EndTimeProperty =
- AvaloniaProperty.Register<Block, TimeSpan>(nameof(EndTime));
- public int NColumns { get; set; } = -1;
- public object Column
- {
- get => GetValue(ColumnProperty);
- set => SetValue(ColumnProperty, value);
- }
- public TimeSpan StartTime
- {
- get => GetValue(StartTimeProperty);
- set => SetValue(StartTimeProperty, value);
- }
- public TimeSpan EndTime
- {
- get => GetValue(EndTimeProperty);
- set => SetValue(EndTimeProperty, value);
- }
- public override string ToString()
- {
- return $"Block({Column}: {StartTime:hh\\:mm} - {EndTime:hh\\:mm})";
- }
- }
- private class Column
- {
- public List<Block> Blocks { get; set; } = new();
- public List<List<Block>>? Columns { get; set; } = null;
- }
- private Dictionary<object, Column> _blocks = new();
- private List<IDisposable> _oldSubscriptions = new();
- private List<object> _columns = new();
- private void RecreateBlocksList()
- {
- if (ItemsSource is null) return;
- var dateBinding = ColumnMapping;
- var startBinding = StartTimeMapping;
- var endBinding = EndTimeMapping;
- if(dateBinding is null || startBinding is null || endBinding is null)
- {
- return;
- }
- foreach(var subscription in _oldSubscriptions)
- {
- subscription.Dispose();
- }
- _oldSubscriptions.Clear();
- _blocks.Clear();
- _columns.Clear();
- var autoGenerateColumns = true;
- if(Columns is not null)
- {
- autoGenerateColumns = false;
- foreach(var column in Columns)
- {
- if(column is null) continue;
- if(!_blocks.TryAdd(column, new()))
- {
- throw new Exception($"Duplicate column {column} in Calendar");
- }
- _columns.Add(column);
- }
- }
- foreach(var item in ItemsSource)
- {
- if (item is null) continue;
- var block = new Block
- {
- [!Block.ColumnProperty] = dateBinding,
- [!Block.StartTimeProperty] = startBinding,
- [!Block.EndTimeProperty] = endBinding,
- [Block.DataContextProperty] = item,
- [!Block.ContentTemplateProperty] = this[!ItemTemplateProperty],
- Content = item
- };
- block.Background = new SolidColorBrush(Colors.Transparent);
- var column = block.Column;
- if(column is null)
- {
- continue;
- }
- if(!_blocks.TryGetValue(column, out var columnBlocks))
- {
- if (!autoGenerateColumns) continue;
- columnBlocks = new();
- _blocks.Add(column, columnBlocks);
- _columns.Add(column);
- }
- _oldSubscriptions.Add(block.GetObservable(Block.ColumnProperty).Skip(1).Subscribe(x => Render(itemsChanged: true)));
- _oldSubscriptions.Add(block.GetObservable(Block.StartTimeProperty).Skip(1).Subscribe(x => UpdateBlock(block)));
- _oldSubscriptions.Add(block.GetObservable(Block.EndTimeProperty).Skip(1).Subscribe(x => UpdateBlock(block)));
- block.PointerPressed += Block_PointerPressed;
- block.PointerReleased += Block_PointerReleased;
- columnBlocks.Blocks.Add(block);
- }
- }
- private double _colWidth;
- private double _colSpace;
- private double _rowHeight;
- private void Render(bool itemsChanged = false, bool recalculatePositions = false)
- {
- if (itemsChanged)
- {
- RecreateBlocksList();
- }
- if (recalculatePositions)
- {
- foreach (var (column, columnBlocks) in _blocks)
- {
- columnBlocks.Columns = null;
- }
- }
- var nRows = (24 / RowInterval.TotalHours);
- var rowHeight = Math.Max(RowHeight, ScrollViewer.Bounds.Height / nRows);
- Canvas.Children.Clear();
- Canvas.Height = rowHeight * nRows;
- var minColWidth = MinimumColumnWidth;
- var colSpace = 1;
- var nColumns = 0;
- foreach (var (column, columnBlocks) in _blocks)
- {
- columnBlocks.Columns ??= RecalculateBlockPositionsForDay(columnBlocks.Blocks);
- // columnsPerDay = Math.Max(columnsPerDay, columnBlocks.Columns.Count);
- nColumns += columnBlocks.Columns.Count;
- }
- // nColumns = columnsPerDay * _blocks.Count
- var colWidth = (Math.Max((ScrollViewer.Bounds.Width - colSpace * (_blocks.Count - 1)) / nColumns, minColWidth));
- HeaderCanvas.Children.Clear();
- _rowHeight = rowHeight;
- _colWidth = colWidth;
- _colSpace = colSpace;
- var minY = double.MaxValue;
- var colX = 0.0;
- var i = 0;
- foreach(var columnKey in _columns)
- {
- if(!_blocks.TryGetValue(columnKey, out var columnBlocks))
- {
- continue;
- }
- var contentControl = new ContentControl
- {
- Content = columnKey,
- Width = colWidth * columnBlocks.Columns!.Count,
- [!Block.ContentTemplateProperty] = this[!HeaderTemplateProperty],
- };
- Canvas.SetLeft(contentControl, colX);
- HeaderCanvas.Children.Add(contentControl);
- contentControl.SizeChanged += ContentControl_SizeChanged;
- foreach(var column in columnBlocks.Columns!)
- {
- foreach(var block in column)
- {
- var blockY = GetRow(block.StartTime) * rowHeight;
- minY = Math.Min(blockY, minY);
- Canvas.SetTop(block, blockY);
- Canvas.SetLeft(block, colX);
- block.Height = Math.Max((GetRow(block.EndTime) - GetRow(block.StartTime)) * rowHeight, 5);
- block.Width = colWidth * block.NColumns;
- Canvas.Children.Add(block);
- }
- colX += colWidth;
- }
- if(i < _blocks.Count - 1)
- {
- var rectangle = new Rectangle
- {
- Width = 0.75,
- Height = Canvas.Height,
- Fill = new SolidColorBrush(Colors.LightGray)
- };
- Canvas.SetLeft(rectangle, colX);
- Canvas.Children.Add(rectangle);
- var headRectangle = new Rectangle
- {
- Width = 0.75,
- [!Rectangle.HeightProperty] = HeaderCanvas.WhenValueChanged(x => x.Bounds)
- .Select(x => x.Height)
- .ToBinding(),
- Fill = new SolidColorBrush(Colors.LightGray)
- };
- Canvas.SetLeft(headRectangle, colX);
- HeaderCanvas.Children.Add(headRectangle);
- colX += colSpace;
- }
- ++i;
- }
- Canvas.Width = colX;
- HeaderCanvas.Width = colX;
- if(minY == double.MaxValue)
- {
- ScrollViewer.Offset = new(0, 0);
- }
- else
- {
- ScrollViewer.Offset = new(0, Math.Max(minY - RowHeight / 2, 0));
- }
- var lines = new List<Control>();
- LabelCanvas.Children.Clear();
- LabelCanvas.Height = Canvas.Height;
- var y = rowHeight;
- for(var time = RowInterval; time < TimeSpan.FromHours(24); time += RowInterval)
- {
- var rectangle = new Rectangle
- {
- Width = Canvas.Width,
- Height = 0.75,
- Fill = new SolidColorBrush(Colors.LightGray)
- };
- Canvas.SetLeft(rectangle, 0);
- Canvas.SetTop(rectangle, y);
- lines.Add(rectangle);
- var block = new TextBlock
- {
- Text = time.ToString("hh\\:mm"),
- Margin = new(0, -5, 0, 0)
- }.WithClass("ExtraSmall");
- block.SizeChanged += Block_SizeChanged;
- Canvas.SetTop(block, y);
- LabelCanvas.Children.Add(block);
- y += rowHeight;
- }
- Canvas.Children.InsertRange(0, lines);
- }
- private bool TryGetBlockFromPosition(PointerEventArgs e, [NotNullWhen(true)] out object? column, out TimeSpan start, out TimeSpan end)
- {
- var point = e.GetPosition(Canvas);
- var rowIdx = (int)Math.Floor(point.Y / _rowHeight);
- start = RowInterval * rowIdx;
- end = RowInterval * (rowIdx + 1);
- if(start.TotalHours < 0)
- {
- start = TimeSpan.Zero;
- }
- if(end.TotalHours >= 24)
- {
- end = TimeSpan.FromHours(24).Subtract(TimeSpan.FromTicks(1));
- }
- column = null;
- var x = point.X;
- foreach(var columnKey in _columns)
- {
- if (!_blocks.TryGetValue(columnKey, out var columnBlocks)) continue;
- var colWidth = columnBlocks.Columns!.Count * _colWidth + _colSpace;
- if(x < colWidth)
- {
- column = columnKey;
- break;
- }
- else
- {
- x -= colWidth;
- }
- }
- return column is not null;
- }
- private CancellationTokenSource? cts = null;
- private void PressedAction(Action onHeld)
- {
- cts?.Cancel();
- cts = new();
- Task.Delay(1000).ContinueWith(task =>
- {
- cts = null;
- onHeld();
- }, cts.Token, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
- }
- private void ReleasedAction(Action onRelease)
- {
- if(cts is not null)
- {
- cts.Cancel();
- onRelease();
- }
- }
- private void Block_PointerPressed(object? sender, global::Avalonia.Input.PointerPressedEventArgs e)
- {
- if (sender is not Block block) return;
- e.Handled = true;
- PressedAction(() => BlockHeld?.Invoke(this, new(block.Content, block.Column, block.StartTime, block.EndTime)));
- }
- private void Block_PointerReleased(object? sender, PointerReleasedEventArgs e)
- {
- if (sender is not Block block) return;
- e.Handled = true;
- ReleasedAction(() => BlockClicked?.Invoke(this, new(block.Content, block.Column, block.StartTime, block.EndTime)));
- }
- private void Canvas_PointerPressed(object? sender, PointerPressedEventArgs e)
- {
- if (!TryGetBlockFromPosition(e, out var column, out var start, out var end)) return;
- PressedAction(() => BlockHeld?.Invoke(this, new(null, column, start, end)));
- }
- private void Canvas_PointerReleased(object? sender, PointerReleasedEventArgs e)
- {
- if (!TryGetBlockFromPosition(e, out var column, out var start, out var end)) return;
- ReleasedAction(() => BlockClicked?.Invoke(this, new(null, column, start, end)));
- }
- private void ContentControl_SizeChanged(object? sender, SizeChangedEventArgs e)
- {
- HeaderCanvas.Height = HeaderCanvas.Children.Select(x => x.Bounds.Height).Max();
- }
- private void Block_SizeChanged(object? sender, SizeChangedEventArgs e)
- {
- LabelCanvas.Width = LabelCanvas.Children.Select(x => x.Bounds.Width).Max();
- }
- private double GetRow(TimeSpan time)
- {
- return time.TotalHours / RowInterval.TotalHours;
- }
- private static List<List<Block>> RecalculateBlockPositionsForDay(List<Block> dayBlocks)
- {
- dayBlocks.SortBy(x => x.StartTime);
- var columns = new List<List<Block>>();
- var remainingBlocks = dayBlocks;
- while(remainingBlocks.Count > 0)
- {
- // At least one block will be moved, so we can use 1 less than the remaining as capacity.
- var tempRemainingBlocks = new List<Block>(remainingBlocks.Count - 1);
- var newBlocks = new List<Block>(remainingBlocks.Count);
- var curTime = TimeSpan.MinValue;
- Block? curBlock = null;
- foreach(var block in remainingBlocks)
- {
- if(curBlock is not null && block.StartTime < curTime)
- {
- tempRemainingBlocks.Add(block);
- }
- else
- {
- newBlocks.Add(block);
- curTime = block.EndTime;
- curBlock = block;
- }
- }
- columns.Add(newBlocks);
- remainingBlocks = tempRemainingBlocks;
- }
- for(int i = 0; i < columns.Count; ++i)
- {
- foreach(var block in columns[i])
- {
- var nColumns = -1;
- for(int j = i + 1; j < columns.Count; ++j)
- {
- foreach(var block2 in columns[j])
- {
- if(block.StartTime < block2.EndTime && block.EndTime > block2.StartTime)
- {
- nColumns = j - i;
- break;
- }
- }
- if(nColumns > -1)
- {
- break;
- }
- }
- block.NColumns = nColumns > -1 ? nColumns : columns.Count - i;
- }
- }
- if(columns.Count == 0)
- {
- columns.Add(new());
- }
- return columns;
- }
- private void UpdateBlock(Block block)
- {
- Render(recalculatePositions: true);
- }
- }
|