CalendarView.axaml.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. using Avalonia;
  2. using Avalonia.Controls;
  3. using Avalonia.Controls.Shapes;
  4. using Avalonia.Controls.Templates;
  5. using Avalonia.Data;
  6. using Avalonia.Input;
  7. using Avalonia.Media;
  8. using Avalonia.Metadata;
  9. using DynamicData.Binding;
  10. using InABox.Core;
  11. using System.Collections;
  12. using System.Collections.Specialized;
  13. using System.Diagnostics.CodeAnalysis;
  14. using System.Reactive.Linq;
  15. using System.Runtime.Serialization;
  16. namespace InABox.Avalonia.Components;
  17. public class CalendarBlockEventArgs(object? value, object column, TimeSpan start, TimeSpan end) : EventArgs
  18. {
  19. public object? Value { get; set; } = value;
  20. public object Column { get; set; } = column;
  21. public TimeSpan Start { get; set; } = start;
  22. public TimeSpan End { get; set; } = end;
  23. }
  24. public partial class CalendarView : UserControl
  25. {
  26. public static readonly StyledProperty<double> RowHeightProperty =
  27. AvaloniaProperty.Register<CalendarView, double>(nameof(RowHeight), defaultValue: 100);
  28. public static readonly StyledProperty<double> MinimumColumnWidthProperty =
  29. AvaloniaProperty.Register<CalendarView, double>(nameof(MinimumColumnWidth), defaultValue: 50);
  30. public static readonly StyledProperty<TimeSpan> RowIntervalProperty =
  31. AvaloniaProperty.Register<CalendarView, TimeSpan>(nameof(RowInterval), defaultValue: TimeSpan.FromHours(1));
  32. public static readonly StyledProperty<IBinding?> ColumnMappingProperty =
  33. AvaloniaProperty.Register<CalendarView, IBinding?>(nameof(ColumnMapping));
  34. public static readonly StyledProperty<IBinding?> StartTimeMappingProperty =
  35. AvaloniaProperty.Register<CalendarView, IBinding?>(nameof(StartTimeMapping));
  36. public static readonly StyledProperty<IBinding?> EndTimeMappingProperty =
  37. AvaloniaProperty.Register<CalendarView, IBinding?>(nameof(EndTimeMapping));
  38. public static readonly StyledProperty<IEnumerable?> ItemsSourceProperty =
  39. AvaloniaProperty.Register<CalendarView, IEnumerable?>(nameof(ItemsSource));
  40. public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty =
  41. AvaloniaProperty.Register<CalendarView, IDataTemplate?>(nameof(ItemTemplate));
  42. public static readonly StyledProperty<IDataTemplate?> HeaderTemplateProperty =
  43. AvaloniaProperty.Register<CalendarView, IDataTemplate?>(nameof(HeaderTemplate));
  44. public static readonly StyledProperty<IEnumerable?> ColumnsProperty =
  45. AvaloniaProperty.Register<CalendarView, IEnumerable?>(nameof(Columns));
  46. public static readonly StyledProperty<bool> ShowColumnsProperty =
  47. AvaloniaProperty.Register<CalendarView, bool>(nameof(ShowColumns));
  48. public double MinimumColumnWidth
  49. {
  50. get => GetValue(MinimumColumnWidthProperty);
  51. set => SetValue(MinimumColumnWidthProperty, value);
  52. }
  53. public double RowHeight
  54. {
  55. get => GetValue(RowHeightProperty);
  56. set => SetValue(RowHeightProperty, value);
  57. }
  58. public TimeSpan RowInterval
  59. {
  60. get => GetValue(RowIntervalProperty);
  61. set => SetValue(RowIntervalProperty, value);
  62. }
  63. [AssignBinding]
  64. [InheritDataTypeFromItems(nameof(ItemsSource))]
  65. public IBinding? ColumnMapping
  66. {
  67. get => GetValue(ColumnMappingProperty);
  68. set => SetValue(ColumnMappingProperty, value);
  69. }
  70. [AssignBinding]
  71. [InheritDataTypeFromItems(nameof(ItemsSource))]
  72. public IBinding? StartTimeMapping
  73. {
  74. get => GetValue(StartTimeMappingProperty);
  75. set => SetValue(StartTimeMappingProperty, value);
  76. }
  77. [AssignBinding]
  78. [InheritDataTypeFromItems(nameof(ItemsSource))]
  79. public IBinding? EndTimeMapping
  80. {
  81. get => GetValue(EndTimeMappingProperty);
  82. set => SetValue(EndTimeMappingProperty, value);
  83. }
  84. public IEnumerable? ItemsSource
  85. {
  86. get => GetValue(ItemsSourceProperty);
  87. set => SetValue(ItemsSourceProperty, value);
  88. }
  89. [InheritDataTypeFromItems(nameof(ItemsSource))]
  90. public IDataTemplate? ItemTemplate
  91. {
  92. get => GetValue(ItemTemplateProperty);
  93. set => SetValue(ItemTemplateProperty, value);
  94. }
  95. public IDataTemplate? HeaderTemplate
  96. {
  97. get => GetValue(HeaderTemplateProperty);
  98. set => SetValue(HeaderTemplateProperty, value);
  99. }
  100. public IEnumerable? Columns
  101. {
  102. get => GetValue(ColumnsProperty);
  103. set => SetValue(ColumnsProperty, value);
  104. }
  105. public bool ShowColumns
  106. {
  107. get => GetValue(ShowColumnsProperty);
  108. set => SetValue(ShowColumnsProperty, value);
  109. }
  110. public event EventHandler<CalendarBlockEventArgs>? BlockClicked;
  111. public event EventHandler<CalendarBlockEventArgs>? BlockHeld;
  112. static CalendarView()
  113. {
  114. ItemsSourceProperty.Changed.AddClassHandler<CalendarView>(ItemsSource_Changed);
  115. RowHeightProperty.Changed.AddClassHandler<CalendarView>(Render_Changed);
  116. MinimumColumnWidthProperty.Changed.AddClassHandler<CalendarView>(Render_Changed);
  117. RowIntervalProperty.Changed.AddClassHandler<CalendarView>(Render_Changed);
  118. ColumnsProperty.Changed.AddClassHandler<CalendarView>(Columns_Changed);
  119. }
  120. private static void Columns_Changed(CalendarView view, AvaloniaPropertyChangedEventArgs args)
  121. {
  122. if(args.OldValue is INotifyCollectionChanged oldNotify)
  123. {
  124. oldNotify.CollectionChanged -= view.ColumnsCollection_Changed;
  125. }
  126. view.Render(itemsChanged: true);
  127. if(args.NewValue is INotifyCollectionChanged notify)
  128. {
  129. notify.CollectionChanged += view.ColumnsCollection_Changed;
  130. }
  131. }
  132. private void ColumnsCollection_Changed(object? sender, NotifyCollectionChangedEventArgs e)
  133. {
  134. Render(itemsChanged: true);
  135. }
  136. private static void Render_Changed(CalendarView view, AvaloniaPropertyChangedEventArgs args)
  137. {
  138. view.Render();
  139. }
  140. public CalendarView()
  141. {
  142. InitializeComponent();
  143. ScrollViewer.GetPropertyChangedObservable(ScrollViewer.OffsetProperty).Subscribe(x =>
  144. {
  145. LabelScroll.Offset = new(0, ScrollViewer.Offset.Y);
  146. HeaderScroll.Offset = new(ScrollViewer.Offset.X, 0);
  147. });
  148. }
  149. private static void ItemsSource_Changed(CalendarView view, AvaloniaPropertyChangedEventArgs args)
  150. {
  151. if(args.OldValue is INotifyCollectionChanged oldNotify)
  152. {
  153. oldNotify.CollectionChanged -= view.Collection_Changed;
  154. }
  155. view.Render(itemsChanged: true);
  156. if(view.ItemsSource is INotifyCollectionChanged notify)
  157. {
  158. notify.CollectionChanged += view.Collection_Changed;
  159. }
  160. }
  161. private void Collection_Changed(object? sender, NotifyCollectionChangedEventArgs e)
  162. {
  163. Render(itemsChanged: true);
  164. }
  165. private void Scroll_SizeChanged(object? sender, SizeChangedEventArgs e)
  166. {
  167. Render();
  168. }
  169. private class Block : ContentControl
  170. {
  171. public static readonly StyledProperty<object> ColumnProperty =
  172. AvaloniaProperty.Register<Block, object>(nameof(Column));
  173. public static readonly StyledProperty<TimeSpan> StartTimeProperty =
  174. AvaloniaProperty.Register<Block, TimeSpan>(nameof(StartTime));
  175. public static readonly StyledProperty<TimeSpan> EndTimeProperty =
  176. AvaloniaProperty.Register<Block, TimeSpan>(nameof(EndTime));
  177. public int NColumns { get; set; } = -1;
  178. public object Column
  179. {
  180. get => GetValue(ColumnProperty);
  181. set => SetValue(ColumnProperty, value);
  182. }
  183. public TimeSpan StartTime
  184. {
  185. get => GetValue(StartTimeProperty);
  186. set => SetValue(StartTimeProperty, value);
  187. }
  188. public TimeSpan EndTime
  189. {
  190. get => GetValue(EndTimeProperty);
  191. set => SetValue(EndTimeProperty, value);
  192. }
  193. public override string ToString()
  194. {
  195. return $"Block({Column}: {StartTime:hh\\:mm} - {EndTime:hh\\:mm})";
  196. }
  197. }
  198. private class Column
  199. {
  200. public List<Block> Blocks { get; set; } = new();
  201. public List<List<Block>>? Columns { get; set; } = null;
  202. }
  203. private Dictionary<object, Column> _blocks = new();
  204. private List<IDisposable> _oldSubscriptions = new();
  205. private List<object> _columns = new();
  206. private void RecreateBlocksList()
  207. {
  208. if (ItemsSource is null) return;
  209. var dateBinding = ColumnMapping;
  210. var startBinding = StartTimeMapping;
  211. var endBinding = EndTimeMapping;
  212. if(dateBinding is null || startBinding is null || endBinding is null)
  213. {
  214. return;
  215. }
  216. foreach(var subscription in _oldSubscriptions)
  217. {
  218. subscription.Dispose();
  219. }
  220. _oldSubscriptions.Clear();
  221. _blocks.Clear();
  222. _columns.Clear();
  223. var autoGenerateColumns = true;
  224. if(Columns is not null)
  225. {
  226. autoGenerateColumns = false;
  227. foreach(var column in Columns)
  228. {
  229. if(column is null) continue;
  230. if(!_blocks.TryAdd(column, new()))
  231. {
  232. throw new Exception($"Duplicate column {column} in Calendar");
  233. }
  234. _columns.Add(column);
  235. }
  236. }
  237. foreach(var item in ItemsSource)
  238. {
  239. if (item is null) continue;
  240. var block = new Block
  241. {
  242. [!Block.ColumnProperty] = dateBinding,
  243. [!Block.StartTimeProperty] = startBinding,
  244. [!Block.EndTimeProperty] = endBinding,
  245. [Block.DataContextProperty] = item,
  246. [!Block.ContentTemplateProperty] = this[!ItemTemplateProperty],
  247. Content = item
  248. };
  249. block.Background = new SolidColorBrush(Colors.Transparent);
  250. var column = block.Column;
  251. if(column is null)
  252. {
  253. continue;
  254. }
  255. if(!_blocks.TryGetValue(column, out var columnBlocks))
  256. {
  257. if (!autoGenerateColumns) continue;
  258. columnBlocks = new();
  259. _blocks.Add(column, columnBlocks);
  260. _columns.Add(column);
  261. }
  262. _oldSubscriptions.Add(block.GetObservable(Block.ColumnProperty).Skip(1).Subscribe(x => Render(itemsChanged: true)));
  263. _oldSubscriptions.Add(block.GetObservable(Block.StartTimeProperty).Skip(1).Subscribe(x => UpdateBlock(block)));
  264. _oldSubscriptions.Add(block.GetObservable(Block.EndTimeProperty).Skip(1).Subscribe(x => UpdateBlock(block)));
  265. block.PointerPressed += Block_PointerPressed;
  266. block.PointerReleased += Block_PointerReleased;
  267. columnBlocks.Blocks.Add(block);
  268. }
  269. }
  270. private double _colWidth;
  271. private double _colSpace;
  272. private double _rowHeight;
  273. private void Render(bool itemsChanged = false, bool recalculatePositions = false)
  274. {
  275. if (itemsChanged)
  276. {
  277. RecreateBlocksList();
  278. }
  279. if (recalculatePositions)
  280. {
  281. foreach (var (column, columnBlocks) in _blocks)
  282. {
  283. columnBlocks.Columns = null;
  284. }
  285. }
  286. var nRows = (24 / RowInterval.TotalHours);
  287. var rowHeight = Math.Max(RowHeight, ScrollViewer.Bounds.Height / nRows);
  288. Canvas.Children.Clear();
  289. Canvas.Height = rowHeight * nRows;
  290. var minColWidth = MinimumColumnWidth;
  291. var colSpace = 1;
  292. var nColumns = 0;
  293. foreach (var (column, columnBlocks) in _blocks)
  294. {
  295. columnBlocks.Columns ??= RecalculateBlockPositionsForDay(columnBlocks.Blocks);
  296. // columnsPerDay = Math.Max(columnsPerDay, columnBlocks.Columns.Count);
  297. nColumns += columnBlocks.Columns.Count;
  298. }
  299. // nColumns = columnsPerDay * _blocks.Count
  300. var colWidth = (Math.Max((ScrollViewer.Bounds.Width - colSpace * (_blocks.Count - 1)) / nColumns, minColWidth));
  301. HeaderCanvas.Children.Clear();
  302. _rowHeight = rowHeight;
  303. _colWidth = colWidth;
  304. _colSpace = colSpace;
  305. var minY = double.MaxValue;
  306. var colX = 0.0;
  307. var i = 0;
  308. foreach(var columnKey in _columns)
  309. {
  310. if(!_blocks.TryGetValue(columnKey, out var columnBlocks))
  311. {
  312. continue;
  313. }
  314. var contentControl = new ContentControl
  315. {
  316. Content = columnKey,
  317. Width = colWidth * columnBlocks.Columns!.Count,
  318. [!Block.ContentTemplateProperty] = this[!HeaderTemplateProperty],
  319. };
  320. Canvas.SetLeft(contentControl, colX);
  321. HeaderCanvas.Children.Add(contentControl);
  322. contentControl.SizeChanged += ContentControl_SizeChanged;
  323. foreach(var column in columnBlocks.Columns!)
  324. {
  325. foreach(var block in column)
  326. {
  327. var blockY = GetRow(block.StartTime) * rowHeight;
  328. minY = Math.Min(blockY, minY);
  329. Canvas.SetTop(block, blockY);
  330. Canvas.SetLeft(block, colX);
  331. block.Height = Math.Max((GetRow(block.EndTime) - GetRow(block.StartTime)) * rowHeight, 5);
  332. block.Width = colWidth * block.NColumns;
  333. Canvas.Children.Add(block);
  334. }
  335. colX += colWidth;
  336. }
  337. if(i < _blocks.Count - 1)
  338. {
  339. var rectangle = new Rectangle
  340. {
  341. Width = 0.75,
  342. Height = Canvas.Height,
  343. Fill = new SolidColorBrush(Colors.LightGray)
  344. };
  345. Canvas.SetLeft(rectangle, colX);
  346. Canvas.Children.Add(rectangle);
  347. var headRectangle = new Rectangle
  348. {
  349. Width = 0.75,
  350. [!Rectangle.HeightProperty] = HeaderCanvas.WhenValueChanged(x => x.Bounds)
  351. .Select(x => x.Height)
  352. .ToBinding(),
  353. Fill = new SolidColorBrush(Colors.LightGray)
  354. };
  355. Canvas.SetLeft(headRectangle, colX);
  356. HeaderCanvas.Children.Add(headRectangle);
  357. colX += colSpace;
  358. }
  359. ++i;
  360. }
  361. Canvas.Width = colX;
  362. HeaderCanvas.Width = colX;
  363. if(minY == double.MaxValue)
  364. {
  365. ScrollViewer.Offset = new(0, 0);
  366. }
  367. else
  368. {
  369. ScrollViewer.Offset = new(0, Math.Max(minY - RowHeight / 2, 0));
  370. }
  371. var lines = new List<Control>();
  372. LabelCanvas.Children.Clear();
  373. LabelCanvas.Height = Canvas.Height;
  374. var y = rowHeight;
  375. for(var time = RowInterval; time < TimeSpan.FromHours(24); time += RowInterval)
  376. {
  377. var rectangle = new Rectangle
  378. {
  379. Width = Canvas.Width,
  380. Height = 0.75,
  381. Fill = new SolidColorBrush(Colors.LightGray)
  382. };
  383. Canvas.SetLeft(rectangle, 0);
  384. Canvas.SetTop(rectangle, y);
  385. lines.Add(rectangle);
  386. var block = new TextBlock
  387. {
  388. Text = time.ToString("hh\\:mm"),
  389. Margin = new(0, -5, 0, 0)
  390. }.WithClass("ExtraSmall");
  391. block.SizeChanged += Block_SizeChanged;
  392. Canvas.SetTop(block, y);
  393. LabelCanvas.Children.Add(block);
  394. y += rowHeight;
  395. }
  396. Canvas.Children.InsertRange(0, lines);
  397. }
  398. private bool TryGetBlockFromPosition(PointerEventArgs e, [NotNullWhen(true)] out object? column, out TimeSpan start, out TimeSpan end)
  399. {
  400. var point = e.GetPosition(Canvas);
  401. var rowIdx = (int)Math.Floor(point.Y / _rowHeight);
  402. start = RowInterval * rowIdx;
  403. end = RowInterval * (rowIdx + 1);
  404. if(start.TotalHours < 0)
  405. {
  406. start = TimeSpan.Zero;
  407. }
  408. if(end.TotalHours >= 24)
  409. {
  410. end = TimeSpan.FromHours(24).Subtract(TimeSpan.FromTicks(1));
  411. }
  412. column = null;
  413. var x = point.X;
  414. foreach(var columnKey in _columns)
  415. {
  416. if (!_blocks.TryGetValue(columnKey, out var columnBlocks)) continue;
  417. var colWidth = columnBlocks.Columns!.Count * _colWidth + _colSpace;
  418. if(x < colWidth)
  419. {
  420. column = columnKey;
  421. break;
  422. }
  423. else
  424. {
  425. x -= colWidth;
  426. }
  427. }
  428. return column is not null;
  429. }
  430. private CancellationTokenSource? cts = null;
  431. private void PressedAction(Action onHeld)
  432. {
  433. cts?.Cancel();
  434. cts = new();
  435. Task.Delay(1000).ContinueWith(task =>
  436. {
  437. cts = null;
  438. onHeld();
  439. }, cts.Token, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
  440. }
  441. private void ReleasedAction(Action onRelease)
  442. {
  443. if(cts is not null)
  444. {
  445. cts.Cancel();
  446. onRelease();
  447. }
  448. }
  449. private void Block_PointerPressed(object? sender, global::Avalonia.Input.PointerPressedEventArgs e)
  450. {
  451. if (sender is not Block block) return;
  452. e.Handled = true;
  453. PressedAction(() => BlockHeld?.Invoke(this, new(block.Content, block.Column, block.StartTime, block.EndTime)));
  454. }
  455. private void Block_PointerReleased(object? sender, PointerReleasedEventArgs e)
  456. {
  457. if (sender is not Block block) return;
  458. e.Handled = true;
  459. ReleasedAction(() => BlockClicked?.Invoke(this, new(block.Content, block.Column, block.StartTime, block.EndTime)));
  460. }
  461. private void Canvas_PointerPressed(object? sender, PointerPressedEventArgs e)
  462. {
  463. if (!TryGetBlockFromPosition(e, out var column, out var start, out var end)) return;
  464. PressedAction(() => BlockHeld?.Invoke(this, new(null, column, start, end)));
  465. }
  466. private void Canvas_PointerReleased(object? sender, PointerReleasedEventArgs e)
  467. {
  468. if (!TryGetBlockFromPosition(e, out var column, out var start, out var end)) return;
  469. ReleasedAction(() => BlockClicked?.Invoke(this, new(null, column, start, end)));
  470. }
  471. private void ContentControl_SizeChanged(object? sender, SizeChangedEventArgs e)
  472. {
  473. HeaderCanvas.Height = HeaderCanvas.Children.Select(x => x.Bounds.Height).Max();
  474. }
  475. private void Block_SizeChanged(object? sender, SizeChangedEventArgs e)
  476. {
  477. LabelCanvas.Width = LabelCanvas.Children.Select(x => x.Bounds.Width).Max();
  478. }
  479. private double GetRow(TimeSpan time)
  480. {
  481. return time.TotalHours / RowInterval.TotalHours;
  482. }
  483. private static List<List<Block>> RecalculateBlockPositionsForDay(List<Block> dayBlocks)
  484. {
  485. dayBlocks.SortBy(x => x.StartTime);
  486. var columns = new List<List<Block>>();
  487. var remainingBlocks = dayBlocks;
  488. while(remainingBlocks.Count > 0)
  489. {
  490. // At least one block will be moved, so we can use 1 less than the remaining as capacity.
  491. var tempRemainingBlocks = new List<Block>(remainingBlocks.Count - 1);
  492. var newBlocks = new List<Block>(remainingBlocks.Count);
  493. var curTime = TimeSpan.MinValue;
  494. Block? curBlock = null;
  495. foreach(var block in remainingBlocks)
  496. {
  497. if(curBlock is not null && block.StartTime < curTime)
  498. {
  499. tempRemainingBlocks.Add(block);
  500. }
  501. else
  502. {
  503. newBlocks.Add(block);
  504. curTime = block.EndTime;
  505. curBlock = block;
  506. }
  507. }
  508. columns.Add(newBlocks);
  509. remainingBlocks = tempRemainingBlocks;
  510. }
  511. for(int i = 0; i < columns.Count; ++i)
  512. {
  513. foreach(var block in columns[i])
  514. {
  515. var nColumns = -1;
  516. for(int j = i + 1; j < columns.Count; ++j)
  517. {
  518. foreach(var block2 in columns[j])
  519. {
  520. if(block.StartTime < block2.EndTime && block.EndTime > block2.StartTime)
  521. {
  522. nColumns = j - i;
  523. break;
  524. }
  525. }
  526. if(nColumns > -1)
  527. {
  528. break;
  529. }
  530. }
  531. block.NColumns = nColumns > -1 ? nColumns : columns.Count - i;
  532. }
  533. }
  534. if(columns.Count == 0)
  535. {
  536. columns.Add(new());
  537. }
  538. return columns;
  539. }
  540. private void UpdateBlock(Block block)
  541. {
  542. Render(recalculatePositions: true);
  543. }
  544. }