CalendarControl.cs 39 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162
  1. using InABox.Core;
  2. using InABox.Wpf;
  3. using InABox.WPF;
  4. using Microsoft.CodeAnalysis.VisualBasic.Syntax;
  5. using NPOI.OpenXmlFormats.Spreadsheet;
  6. using System;
  7. using System.Collections;
  8. using System.Collections.Generic;
  9. using System.Collections.Specialized;
  10. using System.ComponentModel;
  11. using System.Diagnostics.CodeAnalysis;
  12. using System.Linq;
  13. using System.Reactive.Linq;
  14. using System.Text;
  15. using System.Threading;
  16. using System.Threading.Tasks;
  17. using System.Windows;
  18. using System.Windows.Controls;
  19. using System.Windows.Controls.Primitives;
  20. using System.Windows.Data;
  21. using System.Windows.Input;
  22. using System.Windows.Media;
  23. using System.Windows.Media.Animation;
  24. using System.Windows.Shapes;
  25. namespace InABox.WPF;
  26. public class CalendarBlockEventArgs(object? value, object column, DateTime date, TimeSpan start, TimeSpan end) : EventArgs
  27. {
  28. public object? Value { get; set; } = value;
  29. public DateTime Date { get; set; } = date;
  30. public object Column { get; set; } = column;
  31. public TimeSpan Start { get; set; } = start;
  32. public TimeSpan End { get; set; } = end;
  33. }
  34. public class CalendarControl : ContentControl
  35. {
  36. public static readonly DependencyProperty RowHeightProperty =
  37. DependencyProperty.Register(nameof(RowHeight), typeof(double), typeof(CalendarControl), new(100.0, Render_Changed));
  38. public static readonly DependencyProperty MinimumColumnWidthProperty =
  39. DependencyProperty.Register(nameof(MinimumColumnWidth), typeof(double), typeof(CalendarControl), new(50.0, Render_Changed));
  40. public static readonly DependencyProperty RowIntervalProperty =
  41. DependencyProperty.Register(nameof(RowInterval), typeof(TimeSpan), typeof(CalendarControl), new(TimeSpan.FromHours(1), Render_Changed));
  42. public static readonly DependencyProperty ItemsSourceProperty =
  43. DependencyProperty.Register(nameof(ItemsSource), typeof(IEnumerable), typeof(CalendarControl), new(ItemsSource_Changed));
  44. public static readonly DependencyProperty ItemTemplateProperty =
  45. DependencyProperty.Register(nameof(ItemTemplate), typeof(DataTemplate), typeof(CalendarControl));
  46. public static readonly DependencyProperty DateTemplateProperty =
  47. DependencyProperty.Register(nameof(DateTemplate), typeof(DataTemplate), typeof(CalendarControl));
  48. public static readonly DependencyProperty HeaderTemplateProperty =
  49. DependencyProperty.Register(nameof(HeaderTemplate), typeof(DataTemplate), typeof(CalendarControl));
  50. public static readonly DependencyProperty ColumnsProperty =
  51. DependencyProperty.Register(nameof(Columns), typeof(IEnumerable), typeof(CalendarControl), new(Columns_Changed));
  52. public double MinimumColumnWidth
  53. {
  54. get => (double)GetValue(MinimumColumnWidthProperty);
  55. set => SetValue(MinimumColumnWidthProperty, value);
  56. }
  57. public double RowHeight
  58. {
  59. get => (double)GetValue(RowHeightProperty);
  60. set => SetValue(RowHeightProperty, value);
  61. }
  62. public TimeSpan RowInterval
  63. {
  64. get => (TimeSpan)GetValue(RowIntervalProperty);
  65. set => SetValue(RowIntervalProperty, value);
  66. }
  67. public BindingBase? DateMapping { get; set; }
  68. public BindingBase? ColumnMapping { get; set; }
  69. public BindingBase? StartTimeMapping { get; set; }
  70. public BindingBase? EndTimeMapping { get; set; }
  71. public IEnumerable? ItemsSource
  72. {
  73. get => GetValue(ItemsSourceProperty) as IEnumerable;
  74. set => SetValue(ItemsSourceProperty, value);
  75. }
  76. public DataTemplate? ItemTemplate
  77. {
  78. get => GetValue(ItemTemplateProperty) as DataTemplate;
  79. set => SetValue(ItemTemplateProperty, value);
  80. }
  81. public DataTemplate? DateTemplate
  82. {
  83. get => GetValue(DateTemplateProperty) as DataTemplate;
  84. set => SetValue(DateTemplateProperty, value);
  85. }
  86. public DataTemplate? HeaderTemplate
  87. {
  88. get => GetValue(HeaderTemplateProperty) as DataTemplate;
  89. set => SetValue(HeaderTemplateProperty, value);
  90. }
  91. public IEnumerable? Columns
  92. {
  93. get => GetValue(ColumnsProperty) as IEnumerable;
  94. set => SetValue(ColumnsProperty, value);
  95. }
  96. public event EventHandler<CalendarBlockEventArgs>? BlockClicked;
  97. public event EventHandler<CalendarBlockEventArgs>? BlockHeld;
  98. private static void Columns_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
  99. {
  100. if (d is not CalendarControl calendar) return;
  101. if(e.OldValue is INotifyCollectionChanged oldNotify)
  102. {
  103. oldNotify.CollectionChanged -= calendar.ColumnsCollection_Changed;
  104. }
  105. calendar.Render(columnsChanged: true);
  106. if(e.NewValue is INotifyCollectionChanged notify)
  107. {
  108. notify.CollectionChanged += calendar.ColumnsCollection_Changed;
  109. }
  110. }
  111. private void ColumnsCollection_Changed(object? sender, NotifyCollectionChangedEventArgs e)
  112. {
  113. Render(columnsChanged: true);
  114. }
  115. private static void Render_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
  116. {
  117. if (d is not CalendarControl calendar) return;
  118. calendar.Render();
  119. }
  120. private ScrollViewer DateScroll;
  121. private ScrollViewer HeaderScroll;
  122. private ScrollViewer LabelScroll;
  123. private ScrollViewer MainScroll;
  124. private ScrollBar VerticalScroll;
  125. private ScrollBar HorizontalScroll;
  126. private Canvas DateCanvas;
  127. private Canvas HeaderCanvas;
  128. private Canvas LabelCanvas;
  129. private Canvas MainCanvas;
  130. private Border HeaderBorder;
  131. public CalendarControl()
  132. {
  133. var grid = new Grid();
  134. grid.AddRow(GridUnitType.Auto); // Date
  135. grid.AddRow(GridUnitType.Auto); // Column
  136. grid.AddRow(GridUnitType.Star);
  137. grid.AddRow(GridUnitType.Auto); // Scroll Bar
  138. grid.AddColumn(GridUnitType.Auto); // Times
  139. grid.AddColumn(GridUnitType.Star);
  140. grid.AddColumn(GridUnitType.Auto); // ScrollBar
  141. DateScroll = new ScrollViewer
  142. {
  143. HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
  144. VerticalScrollBarVisibility = ScrollBarVisibility.Disabled
  145. };
  146. DateCanvas = new Canvas
  147. {
  148. };
  149. DateScroll.Content = DateCanvas;
  150. HeaderScroll = new ScrollViewer
  151. {
  152. HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
  153. VerticalScrollBarVisibility = ScrollBarVisibility.Disabled
  154. };
  155. HeaderCanvas = new Canvas
  156. {
  157. };
  158. HeaderScroll.Content = HeaderCanvas;
  159. LabelScroll = new ScrollViewer
  160. {
  161. VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
  162. HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled
  163. };
  164. LabelCanvas = new Canvas
  165. {
  166. Margin = new(2, 0, 2, 0)
  167. };
  168. LabelScroll.Content = LabelCanvas;
  169. MainScroll = new ScrollViewer
  170. {
  171. HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
  172. VerticalScrollBarVisibility = ScrollBarVisibility.Hidden
  173. };
  174. MainCanvas = new Canvas
  175. {
  176. };
  177. MainScroll.Content = MainCanvas;
  178. MainScroll.SizeChanged += MainScroll_SizeChanged;
  179. MainScroll.ScrollChanged += MainScroll_ScrollChanged;
  180. MainScroll.PreviewMouseWheel += MainScroll_PreviewMouseWheel;
  181. grid.AddChild(
  182. new Border
  183. {
  184. BorderBrush = Colors.LightGray.ToBrush(),
  185. BorderThickness = new(0, 0, 1, 0)
  186. },
  187. row: 0, rowSpan: 2,
  188. column: 0);
  189. grid.AddChild(
  190. new Border
  191. {
  192. BorderBrush = Colors.LightGray.ToBrush(),
  193. BorderThickness = new(0, 0, 0, 1),
  194. Child = DateScroll
  195. },
  196. row: 0,
  197. column: 1, colSpan: 2);
  198. HeaderBorder = new Border
  199. {
  200. BorderBrush = Colors.LightGray.ToBrush(),
  201. BorderThickness = new(0, 0, 0, 1),
  202. Child = HeaderScroll
  203. };
  204. grid.AddChild(
  205. HeaderBorder,
  206. row: 1,
  207. column: 1, colSpan: 2);
  208. grid.AddChild(
  209. new Border
  210. {
  211. BorderBrush = Colors.LightGray.ToBrush(),
  212. BorderThickness = new(0, 0, 1, 0),
  213. Child = LabelScroll
  214. },
  215. row: 2, rowSpan: 2,
  216. column: 0);
  217. grid.AddChild(
  218. new Border
  219. {
  220. Child = MainScroll
  221. },
  222. row: 2, rowSpan: 2,
  223. column: 1, colSpan: 2);
  224. VerticalScroll = new ScrollBar();
  225. VerticalScroll.Scroll += VerticalScroll_Scroll;
  226. VerticalScroll.Opacity = 0.5;
  227. VerticalScroll.MouseEnter += Scroll_MouseEnter;
  228. VerticalScroll.MouseLeave += Scroll_MouseLeave;
  229. HorizontalScroll = new ScrollBar
  230. {
  231. Orientation = Orientation.Horizontal
  232. };
  233. HorizontalScroll.Scroll += HorizontalScroll_Scroll;
  234. HorizontalScroll.Opacity = 0.5;
  235. HorizontalScroll.MouseEnter += Scroll_MouseEnter;
  236. HorizontalScroll.MouseLeave += Scroll_MouseLeave;
  237. grid.AddChild(VerticalScroll, 2, 2);
  238. grid.AddChild(HorizontalScroll, 3, 1);
  239. Content = grid;
  240. }
  241. private (double x, double y) _currentScroll;
  242. private void Scroll_MouseLeave(object sender, MouseEventArgs e)
  243. {
  244. if(sender is ScrollBar bar)
  245. {
  246. bar.Opacity = 0.5;
  247. }
  248. }
  249. private void Scroll_MouseEnter(object sender, MouseEventArgs e)
  250. {
  251. if(sender is ScrollBar bar)
  252. {
  253. bar.Opacity = 1;
  254. }
  255. }
  256. private void MainScroll_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
  257. {
  258. if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
  259. {
  260. MainScroll.ScrollToHorizontalOffset(MainScroll.HorizontalOffset - e.Delta);
  261. e.Handled = true;
  262. }
  263. }
  264. private void HorizontalScroll_Scroll(object sender, ScrollEventArgs e)
  265. {
  266. MainScroll.ScrollToHorizontalOffset(e.NewValue);
  267. }
  268. private void VerticalScroll_Scroll(object sender, ScrollEventArgs e)
  269. {
  270. MainScroll.ScrollToVerticalOffset(e.NewValue);
  271. }
  272. private void MainScroll_ScrollChanged(object sender, ScrollChangedEventArgs e)
  273. {
  274. LabelScroll.ScrollToVerticalOffset(e.VerticalOffset);
  275. HeaderScroll.ScrollToHorizontalOffset(e.HorizontalOffset);
  276. DateScroll.ScrollToHorizontalOffset(e.HorizontalOffset);
  277. HorizontalScroll.Value = e.HorizontalOffset;
  278. VerticalScroll.Value = e.VerticalOffset;
  279. _currentScroll = (e.HorizontalOffset, e.VerticalOffset);
  280. }
  281. private static void ItemsSource_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
  282. {
  283. if (d is not CalendarControl calendar) return;
  284. if(e.OldValue is INotifyCollectionChanged oldNotify)
  285. {
  286. oldNotify.CollectionChanged -= calendar.Collection_Changed;
  287. }
  288. calendar.Render(itemsChanged: true);
  289. if(calendar.ItemsSource is INotifyCollectionChanged notify)
  290. {
  291. notify.CollectionChanged += calendar.Collection_Changed;
  292. }
  293. }
  294. private void Collection_Changed(object? sender, NotifyCollectionChangedEventArgs e)
  295. {
  296. Render(itemsChanged: true);
  297. }
  298. private void MainScroll_SizeChanged(object sender, SizeChangedEventArgs e)
  299. {
  300. Render();
  301. }
  302. private class Block : Border, INotifyPropertyChanged
  303. {
  304. public static readonly DependencyProperty ColumnProperty =
  305. DependencyProperty.Register(nameof(Column), typeof(object), typeof(Block), new(OnPropertyChangedHandler));
  306. public static readonly DependencyProperty DateProperty =
  307. DependencyProperty.Register(nameof(Date), typeof(DateTime), typeof(Block), new(OnPropertyChangedHandler));
  308. public static readonly DependencyProperty StartTimeProperty =
  309. DependencyProperty.Register(nameof(StartTime), typeof(TimeSpan), typeof(Block));
  310. public static readonly DependencyProperty EndTimeProperty =
  311. DependencyProperty.Register(nameof(EndTime), typeof(TimeSpan), typeof(Block));
  312. public int ColumnIndex { get; set; } = -1;
  313. public int NColumns { get; set; } = -1;
  314. public object Column
  315. {
  316. get => GetValue(ColumnProperty);
  317. set => SetValue(ColumnProperty, value);
  318. }
  319. public DateTime Date
  320. {
  321. get => (DateTime)GetValue(DateProperty);
  322. set => SetValue(DateProperty, value);
  323. }
  324. public TimeSpan StartTime
  325. {
  326. get => (TimeSpan)GetValue(StartTimeProperty);
  327. set => SetValue(StartTimeProperty, value);
  328. }
  329. public TimeSpan EndTime
  330. {
  331. get => (TimeSpan)GetValue(EndTimeProperty);
  332. set => SetValue(EndTimeProperty, value);
  333. }
  334. private ContentControl _contentControl;
  335. public ContentControl ContentControl => _contentControl;
  336. public object? Content => _contentControl.Content;
  337. public override string ToString()
  338. {
  339. return $"Block({Column}: {StartTime:hh\\:mm} - {EndTime:hh\\:mm})";
  340. }
  341. public Block(CalendarControl parent, object content)
  342. {
  343. _contentControl = new ContentControl
  344. {
  345. Content = content
  346. };
  347. _contentControl.Bind(ContentControl.ContentTemplateProperty, parent, x => x.ItemTemplate);
  348. Child = _contentControl;
  349. }
  350. public event PropertyChangedEventHandler? PropertyChanged;
  351. private static void OnPropertyChangedHandler(DependencyObject d, DependencyPropertyChangedEventArgs e)
  352. {
  353. if (d is not Block block) return;
  354. block.PropertyChanged?.Invoke(block, new(e.Property.Name));
  355. }
  356. }
  357. private class Column
  358. {
  359. public List<Block> Blocks { get; set; } = new();
  360. public List<List<Block>>? Columns { get; set; } = null;
  361. }
  362. private List<Block> _blockList = new();
  363. private Dictionary<DateTime, Dictionary<object, Column>> _blocks = new();
  364. private List<IDisposable> _oldSubscriptions = new();
  365. private List<object> _columns = new();
  366. private List<(DateTime, object)> _columnList = new();
  367. private IEnumerable<KeyValuePair<(DateTime, object), Column>> _allColumns =>
  368. _blocks.SelectMany(x => x.Value.Select(y => new KeyValuePair<(DateTime, object), Column>((x.Key, y.Key), y.Value)));
  369. private class ActionDisposable(Action onDispose) : IDisposable
  370. {
  371. public void Dispose()
  372. {
  373. onDispose();
  374. }
  375. }
  376. private bool RecreateBlocksList()
  377. {
  378. if (ItemsSource is null) return false;
  379. var columnBinding = ColumnMapping;
  380. var dateBinding = DateMapping;
  381. var startBinding = StartTimeMapping;
  382. var endBinding = EndTimeMapping;
  383. if(columnBinding is null || dateBinding is null || startBinding is null || endBinding is null)
  384. {
  385. return false;
  386. }
  387. foreach(var subscription in _oldSubscriptions)
  388. {
  389. subscription.Dispose();
  390. }
  391. _oldSubscriptions.Clear();
  392. _blockList.Clear();
  393. foreach(var item in ItemsSource)
  394. {
  395. if (item is null) continue;
  396. var block = new Block(this, item);
  397. block.SetBinding(Block.ColumnProperty, columnBinding);
  398. block.SetBinding(Block.StartTimeProperty, startBinding);
  399. block.SetBinding(Block.EndTimeProperty, endBinding);
  400. block.SetBinding(Block.DateProperty, dateBinding);
  401. block.DataContext = item;
  402. block.Background = Colors.Transparent.ToBrush();
  403. block.PropertyChanged += Block_PropertyChanged;
  404. _oldSubscriptions.Add(new ActionDisposable(() => block.PropertyChanged -= Block_PropertyChanged));
  405. block.ContentControl.MouseLeftButtonDown += (o, e) =>
  406. {
  407. Block_MouseLeftButtonDown(block, e);
  408. };
  409. block.ContentControl.MouseLeftButtonUp += (o, e) =>
  410. {
  411. Block_MouseLeftButtonUp(block, e);
  412. };
  413. _blockList.Add(block);
  414. }
  415. return true;
  416. }
  417. private void RefreshColumns(bool itemsChanged = false)
  418. {
  419. if (itemsChanged)
  420. {
  421. if (!RecreateBlocksList()) return;
  422. }
  423. _blocks.Clear();
  424. _columns.Clear();
  425. var autoGenerateColumns = true;
  426. if(Columns is not null)
  427. {
  428. autoGenerateColumns = false;
  429. foreach(var column in Columns)
  430. {
  431. if(column is null) continue;
  432. _columns.Add(column);
  433. }
  434. }
  435. foreach(var block in _blockList)
  436. {
  437. var column = block.Column;
  438. var date = block.Date;
  439. if(column is null)
  440. {
  441. continue;
  442. }
  443. if(!_blocks.TryGetValue(date, out var dateBlocks))
  444. {
  445. dateBlocks = new();
  446. _blocks.Add(date, dateBlocks);
  447. if (!autoGenerateColumns)
  448. {
  449. foreach(var col in _columns)
  450. {
  451. if(!dateBlocks.TryAdd(col, new()))
  452. {
  453. throw new Exception($"Duplicate column {column} in Calendar");
  454. }
  455. }
  456. }
  457. }
  458. if(!dateBlocks.TryGetValue(column, out var columnBlocks))
  459. {
  460. if (!autoGenerateColumns) continue;
  461. columnBlocks = new();
  462. dateBlocks.Add(column, columnBlocks);
  463. _columns.Add(column);
  464. }
  465. columnBlocks.Blocks.Add(block);
  466. }
  467. HeaderBorder.Visibility = _columns.Count <= 1 ? Visibility.Collapsed : Visibility.Visible;
  468. }
  469. private void Block_PropertyChanged(object? sender, PropertyChangedEventArgs e)
  470. {
  471. if (sender is not Block block) return;
  472. if(e.PropertyName == nameof(Block.Column))
  473. {
  474. Render(columnsChanged: true);
  475. }
  476. else if(e.PropertyName == nameof(Block.StartTime)
  477. || e.PropertyName == nameof(Block.EndTime))
  478. {
  479. UpdateBlock(block);
  480. }
  481. }
  482. private double _colWidth;
  483. private double _colSpace;
  484. private double _rowHeight;
  485. private bool _columnsChanged = false;
  486. private bool _itemsChanged = false;
  487. private bool _recalculatePositions = false;
  488. private bool _rerendering = false;
  489. private void Render(bool columnsChanged = false, bool itemsChanged = false, bool recalculatePositions = false)
  490. {
  491. _columnsChanged = _columnsChanged || columnsChanged;
  492. _itemsChanged = _itemsChanged || itemsChanged;
  493. _recalculatePositions = _recalculatePositions || recalculatePositions;
  494. if (!_rerendering)
  495. {
  496. _rerendering = true;
  497. Dispatcher.BeginInvoke(DoRender);
  498. }
  499. }
  500. private double _lastColWidth;
  501. private void DoRender()
  502. {
  503. _rerendering = false;
  504. var itemsChanged = _itemsChanged;
  505. var columnsChanged = _columnsChanged;
  506. var recalculatePositions = _recalculatePositions;
  507. _columnsChanged = false;
  508. _itemsChanged = false;
  509. _recalculatePositions = false;
  510. if (itemsChanged || columnsChanged)
  511. {
  512. RefreshColumns(itemsChanged: itemsChanged);
  513. }
  514. if (recalculatePositions)
  515. {
  516. foreach(var (column, columnBlocks) in _allColumns)
  517. {
  518. columnBlocks.Columns = null;
  519. }
  520. }
  521. var nRows = (24 / RowInterval.TotalHours);
  522. var rowHeight = Math.Max(RowHeight, MainScroll.ActualHeight / nRows);
  523. MainCanvas.Children.Clear();
  524. MainCanvas.Height = rowHeight * nRows;
  525. var minColWidth = MinimumColumnWidth;
  526. var colSpace = 1;
  527. var nColumns = 0;
  528. foreach (var (column, columnBlocks) in _allColumns)
  529. {
  530. columnBlocks.Columns ??= RecalculateBlockPositionsForDay(columnBlocks.Blocks);
  531. // columnsPerDay = Math.Max(columnsPerDay, columnBlocks.Columns.Count);
  532. nColumns += columnBlocks.Columns.Count;
  533. }
  534. // nColumns = columnsPerDay * _blocks.Count
  535. var colWidth = (Math.Max((MainScroll.ActualWidth - colSpace * (_blocks.Sum(x => x.Value.Count) - 1)) / nColumns, minColWidth));
  536. var lastColWidth = _lastColWidth;
  537. _lastColWidth = colWidth;
  538. var updateHeaders = colWidth != lastColWidth || columnsChanged || itemsChanged;
  539. if (updateHeaders)
  540. {
  541. HeaderCanvas.Children.Clear();
  542. DateCanvas.Children.Clear();
  543. }
  544. ClearHeldSelection();
  545. _rowHeight = rowHeight;
  546. _colWidth = colWidth;
  547. _colSpace = colSpace;
  548. var minY = double.MaxValue;
  549. var colX = 0.0;
  550. var dates = _blocks.Keys.ToArray();
  551. Array.Sort(dates);
  552. _columnList.Clear();
  553. var columnIdx = 0;
  554. foreach(var date in dates)
  555. {
  556. if (!_blocks.TryGetValue(date, out var dateBlocks)) continue;
  557. if (updateHeaders)
  558. {
  559. var nDateColumns = dateBlocks.Sum(x => x.Value.Columns!.Count);
  560. var dateHeader = new ContentControl
  561. {
  562. Content = date,
  563. Width = colWidth * nDateColumns + colSpace * (nDateColumns - 1)
  564. };
  565. dateHeader.Bind(ContentControl.ContentTemplateProperty, this, x => x.DateTemplate);
  566. Canvas.SetLeft(dateHeader, colX);
  567. DateCanvas.Children.Add(dateHeader);
  568. dateHeader.SizeChanged += DateHeader_SizeChanged;
  569. }
  570. var dateColumnIndex = 0;
  571. foreach(var columnKey in _columns)
  572. {
  573. if(!dateBlocks.TryGetValue(columnKey, out var columnBlocks))
  574. {
  575. continue;
  576. }
  577. _columnList.Add((date, columnKey));
  578. if (updateHeaders)
  579. {
  580. var columnHeader = new ContentControl
  581. {
  582. Content = columnKey,
  583. Width = colWidth * columnBlocks.Columns!.Count,
  584. };
  585. columnHeader.Bind(ContentControl.ContentTemplateProperty, this, x => x.HeaderTemplate);
  586. Canvas.SetLeft(columnHeader, colX);
  587. HeaderCanvas.Children.Add(columnHeader);
  588. columnHeader.SizeChanged += ColumnHeader_SizeChanged;
  589. }
  590. // Add cell placeholders
  591. var rowIdx = 0;
  592. for(var time = TimeSpan.Zero; time < TimeSpan.FromHours(24); time += RowInterval)
  593. {
  594. var rectangle = new Rectangle
  595. {
  596. Width = colWidth * columnBlocks.Columns!.Count,
  597. Height = RowHeight,
  598. Fill = new SolidColorBrush(Colors.Transparent),
  599. };
  600. rectangle.MouseEnter += (o, e) =>
  601. {
  602. rectangle.Fill = Colors.LightBlue.ToBrush();
  603. };
  604. rectangle.MouseLeave += (o, e) =>
  605. {
  606. rectangle.Fill = Colors.Transparent.ToBrush();
  607. };
  608. rectangle.MouseLeftButtonDown += Rectangle_MouseLeftButtonDown;
  609. rectangle.MouseLeftButtonUp += Rectangle_MouseLeftButtonUp;
  610. Canvas.SetLeft(rectangle, colX);
  611. Canvas.SetTop(rectangle, rowIdx * rowHeight);
  612. MainCanvas.Children.Add(rectangle);
  613. ++rowIdx;
  614. }
  615. foreach(var column in columnBlocks.Columns!)
  616. {
  617. foreach(var block in column)
  618. {
  619. var blockY = GetRow(block.StartTime) * rowHeight;
  620. minY = Math.Min(blockY, minY);
  621. Canvas.SetTop(block, blockY);
  622. Canvas.SetLeft(block, colX);
  623. block.Height = Math.Max((GetRow(block.EndTime) - GetRow(block.StartTime)) * rowHeight, 5);
  624. block.Width = colWidth * block.NColumns;
  625. MainCanvas.Children.Add(block);
  626. }
  627. colX += colWidth;
  628. }
  629. // Add Header separators
  630. if(columnIdx < nColumns - 1)
  631. {
  632. var rectangle = new Rectangle
  633. {
  634. Width = 0.75,
  635. Height = MainCanvas.Height,
  636. Fill = new SolidColorBrush(Colors.LightGray)
  637. };
  638. Canvas.SetLeft(rectangle, colX);
  639. MainCanvas.Children.Add(rectangle);
  640. if (updateHeaders)
  641. {
  642. var headRectangle = new Rectangle
  643. {
  644. Width = 0.75,
  645. Fill = new SolidColorBrush(Colors.LightGray)
  646. };
  647. headRectangle.Bind(Rectangle.HeightProperty, HeaderCanvas, x => x.ActualHeight);
  648. Canvas.SetLeft(headRectangle, colX);
  649. HeaderCanvas.Children.Add(headRectangle);
  650. if(dateColumnIndex == dateBlocks.Count - 1)
  651. {
  652. var dateRectangle = new Rectangle
  653. {
  654. Width = 0.75,
  655. Fill = new SolidColorBrush(Colors.LightGray)
  656. };
  657. dateRectangle.Bind(Rectangle.HeightProperty, DateCanvas, x => x.ActualHeight);
  658. Canvas.SetLeft(dateRectangle, colX);
  659. DateCanvas.Children.Add(dateRectangle);
  660. }
  661. }
  662. colX += colSpace;
  663. }
  664. ++dateColumnIndex;
  665. ++columnIdx;
  666. }
  667. }
  668. MainCanvas.Width = Math.Floor(colX);
  669. HeaderCanvas.Width = Math.Floor(colX);
  670. DateCanvas.Width = Math.Floor(colX);
  671. VerticalScroll.Minimum = 0;
  672. VerticalScroll.Maximum = MainCanvas.Height - MainScroll.ActualHeight;
  673. VerticalScroll.ViewportSize = MainScroll.ActualHeight;
  674. VerticalScroll.Visibility = VerticalScroll.Maximum < 1 ? Visibility.Collapsed : Visibility.Visible;
  675. HorizontalScroll.Minimum = 0;
  676. HorizontalScroll.Maximum = MainCanvas.Width - MainScroll.ActualWidth;
  677. HorizontalScroll.ViewportSize = MainScroll.ActualWidth;
  678. HorizontalScroll.Visibility = HorizontalScroll.Maximum < 1 ? Visibility.Collapsed : Visibility.Visible;
  679. if(minY == double.MaxValue)
  680. {
  681. MainScroll.ScrollToHorizontalOffset(_currentScroll.x);
  682. MainScroll.ScrollToVerticalOffset(_currentScroll.y);
  683. }
  684. else
  685. {
  686. MainScroll.ScrollToHorizontalOffset(_currentScroll.x);
  687. MainScroll.ScrollToVerticalOffset(Math.Max(minY - RowHeight / 2, _currentScroll.y));
  688. }
  689. var lines = new List<FrameworkElement>();
  690. LabelCanvas.Children.Clear();
  691. LabelCanvas.Height = MainCanvas.Height;
  692. var y = rowHeight;
  693. for(var time = RowInterval; time < TimeSpan.FromHours(24); time += RowInterval)
  694. {
  695. var rectangle = new Rectangle
  696. {
  697. Width = MainCanvas.Width,
  698. Height = 0.75,
  699. Fill = new SolidColorBrush(Colors.LightGray)
  700. };
  701. Canvas.SetLeft(rectangle, 0);
  702. Canvas.SetTop(rectangle, y);
  703. lines.Add(rectangle);
  704. var block = new TextBlock
  705. {
  706. Text = time.ToString("hh\\:mm"),
  707. Margin = new(0, -5, 0, 0),
  708. FontSize = 10
  709. };
  710. block.SizeChanged += Block_SizeChanged;
  711. Canvas.SetTop(block, y);
  712. LabelCanvas.Children.Add(block);
  713. y += rowHeight;
  714. }
  715. for(var i = 0; i < lines.Count; ++i)
  716. {
  717. MainCanvas.Children.Insert(i, lines[i]);
  718. }
  719. }
  720. private bool TryGetBlockFromPosition(MouseEventArgs e, out DateTime blockDate, [NotNullWhen(true)] out object? column, out TimeSpan start, out TimeSpan end, out int index)
  721. {
  722. var point = e.GetPosition(MainCanvas);
  723. var rowIdx = (int)Math.Floor(point.Y / _rowHeight);
  724. start = RowInterval * rowIdx;
  725. end = RowInterval * (rowIdx + 1);
  726. if(start.TotalHours < 0)
  727. {
  728. start = TimeSpan.Zero;
  729. }
  730. if(end.TotalHours >= 24)
  731. {
  732. end = TimeSpan.FromHours(24).Subtract(TimeSpan.FromTicks(1));
  733. }
  734. column = null;
  735. index = -1;
  736. blockDate = DateTime.MinValue;
  737. var x = point.X;
  738. foreach(var (date, columnKey) in _columnList)
  739. {
  740. if (!_blocks.TryGetValue(date, out var dateBlocks)
  741. || !dateBlocks.TryGetValue(columnKey, out var columnBlocks)) continue;
  742. var colWidth = columnBlocks.Columns!.Count * _colWidth + _colSpace;
  743. if(x < colWidth)
  744. {
  745. column = columnKey;
  746. index = Math.Min((int)Math.Floor(x / _colWidth), columnBlocks.Columns!.Count - 1);
  747. blockDate = date;
  748. break;
  749. }
  750. else
  751. {
  752. x -= colWidth;
  753. }
  754. }
  755. return column is not null;
  756. }
  757. private CancellationTokenSource? cts = null;
  758. private void PressedAction(Action onHeld)
  759. {
  760. cts?.Cancel();
  761. cts = new();
  762. Task.Delay(1000).ContinueWith(task =>
  763. {
  764. cts = null;
  765. onHeld();
  766. }, cts.Token, TaskContinuationOptions.None, TaskScheduler.FromCurrentSynchronizationContext());
  767. }
  768. private void ReleasedAction(Action onRelease)
  769. {
  770. if(cts is not null)
  771. {
  772. cts.Cancel();
  773. onRelease();
  774. }
  775. }
  776. private void Block_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
  777. {
  778. if (sender is not Block block) return;
  779. e.Handled = true;
  780. PressedAction(() => BlockHeld?.Invoke(this, new(block.Content, block.Column, block.Date, block.StartTime, block.EndTime)));
  781. }
  782. private void Block_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
  783. {
  784. if (sender is not Block block) return;
  785. e.Handled = true;
  786. ReleasedAction(() => BlockClicked?.Invoke(this, new(block.Content, block.Column, block.Date, block.StartTime, block.EndTime)));
  787. }
  788. private Border? _heldSelection;
  789. public void ClearHeldSelection()
  790. {
  791. MainCanvas.Children.Remove(_heldSelection);
  792. _heldSelection = null;
  793. }
  794. private void Rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  795. {
  796. if (sender is not UIElement element) return;
  797. if (!TryGetBlockFromPosition(e, out var date, out var column, out var start, out var end, out var index)) return;
  798. var pos = e.GetPosition(MainCanvas);
  799. ClearHeldSelection();
  800. PressedAction(() =>
  801. {
  802. if(_heldSelection is null)
  803. {
  804. _heldSelection = new Border
  805. {
  806. Background = Colors.Black.ToBrush(0.2)
  807. };
  808. }
  809. else
  810. {
  811. MainCanvas.Children.Remove(_heldSelection);
  812. }
  813. var element = MainCanvas.Children.OfType<Block>().FirstOrDefault(x => x.Date == date && x.Column == column);
  814. if(element is not null)
  815. {
  816. var idx = MainCanvas.Children.IndexOf(element);
  817. MainCanvas.Children.Insert(idx + 1, _heldSelection);
  818. }
  819. else
  820. {
  821. MainCanvas.Children.Add(_heldSelection);
  822. }
  823. var blockStart = start;
  824. var blockEnd = TimeSpan.MinValue;
  825. var x = 0.0;
  826. foreach(var (columnDate, columnKey) in _columnList)
  827. {
  828. if (!_blocks.TryGetValue(columnDate, out var dateBlocks)
  829. || !dateBlocks.TryGetValue(columnKey, out var columnBlocks)) continue;
  830. var colWidth = columnBlocks.Columns!.Count * _colWidth + _colSpace;
  831. if(date == columnDate && column == columnKey)
  832. {
  833. x += index * _colWidth;
  834. var list = columnBlocks.Blocks.Where(x =>
  835. {
  836. return x.ColumnIndex <= index && index < x.ColumnIndex + x.NColumns;
  837. }).ToList();
  838. list.SortBy(x => x.StartTime);
  839. for(int i = 0; i < list.Count; ++i)
  840. {
  841. if(start < list[i].StartTime)
  842. {
  843. blockEnd = list[i].StartTime;
  844. break;
  845. }
  846. else
  847. {
  848. blockStart = list[i].EndTime;
  849. if(i == list.Count - 1)
  850. {
  851. blockEnd = TimeSpan.FromHours(24).Subtract(TimeSpan.FromTicks(1));
  852. }
  853. }
  854. }
  855. break;
  856. }
  857. x += colWidth;
  858. }
  859. if(blockEnd == TimeSpan.MinValue)
  860. {
  861. blockStart = start;
  862. blockEnd = end;
  863. }
  864. var top = (blockStart.TotalHours / RowInterval.TotalHours) * RowHeight;
  865. var height = ((blockEnd - blockStart).TotalHours / RowInterval.TotalHours) * RowHeight;
  866. var width = _colWidth;
  867. var duration = TimeSpan.FromSeconds(0.2);
  868. var leftAnimation = new DoubleAnimation
  869. {
  870. From = pos.X,
  871. To = x,
  872. Duration = duration
  873. };
  874. Storyboard.SetTarget(leftAnimation, _heldSelection);
  875. Storyboard.SetTargetProperty(leftAnimation, new PropertyPath("(Canvas.Left)"));
  876. var widthAnimation = new DoubleAnimation
  877. {
  878. From = 0,
  879. To = width,
  880. Duration = duration
  881. };
  882. Storyboard.SetTarget(widthAnimation, _heldSelection);
  883. Storyboard.SetTargetProperty(widthAnimation, new PropertyPath("Width"));
  884. var topAnimation = new DoubleAnimation
  885. {
  886. From = pos.Y,
  887. To = top,
  888. Duration = duration
  889. };
  890. Storyboard.SetTarget(topAnimation, _heldSelection);
  891. Storyboard.SetTargetProperty(topAnimation, new PropertyPath("(Canvas.Top)"));
  892. var heightAnimation = new DoubleAnimation
  893. {
  894. From = 0,
  895. To = height,
  896. Duration = duration
  897. };
  898. Storyboard.SetTarget(heightAnimation, _heldSelection);
  899. Storyboard.SetTargetProperty(heightAnimation, new PropertyPath("Height"));
  900. var storyBoard = new Storyboard();
  901. storyBoard.Children.Add(leftAnimation);
  902. storyBoard.Children.Add(topAnimation);
  903. storyBoard.Children.Add(widthAnimation);
  904. storyBoard.Children.Add(heightAnimation);
  905. storyBoard.Completed += (o, e) =>
  906. {
  907. BlockHeld?.Invoke(this, new(null, column, date, blockStart, blockEnd));
  908. };
  909. storyBoard.Begin();
  910. });
  911. }
  912. private void Rectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
  913. {
  914. if (!TryGetBlockFromPosition(e, out var date, out var column, out var start, out var end, out var index)) return;
  915. ReleasedAction(() =>
  916. {
  917. BlockClicked?.Invoke(this, new(null, column, date, start, end));
  918. });
  919. }
  920. private void ColumnHeader_SizeChanged(object? sender, SizeChangedEventArgs e)
  921. {
  922. HeaderCanvas.Height = HeaderCanvas.Children.OfType<FrameworkElement>().Select(x => x.ActualHeight).Max();
  923. }
  924. private void DateHeader_SizeChanged(object sender, SizeChangedEventArgs e)
  925. {
  926. DateCanvas.Height = DateCanvas.Children.OfType<FrameworkElement>().Select(x => x.ActualHeight).Max();
  927. }
  928. private void Block_SizeChanged(object? sender, SizeChangedEventArgs e)
  929. {
  930. LabelCanvas.Width = LabelCanvas.Children.OfType<FrameworkElement>().Select(x => x.ActualWidth).Max();
  931. }
  932. private double GetRow(TimeSpan time)
  933. {
  934. return time.TotalHours / RowInterval.TotalHours;
  935. }
  936. private static List<List<Block>> RecalculateBlockPositionsForDay(List<Block> dayBlocks)
  937. {
  938. dayBlocks.SortBy(x => x.StartTime);
  939. var columns = new List<List<Block>>();
  940. var remainingBlocks = dayBlocks;
  941. while(remainingBlocks.Count > 0)
  942. {
  943. // At least one block will be moved, so we can use 1 less than the remaining as capacity.
  944. var tempRemainingBlocks = new List<Block>(remainingBlocks.Count - 1);
  945. var newBlocks = new List<Block>(remainingBlocks.Count);
  946. var curTime = TimeSpan.MinValue;
  947. Block? curBlock = null;
  948. foreach(var block in remainingBlocks)
  949. {
  950. if(curBlock is not null && block.StartTime < curTime)
  951. {
  952. tempRemainingBlocks.Add(block);
  953. }
  954. else
  955. {
  956. newBlocks.Add(block);
  957. curTime = block.EndTime;
  958. curBlock = block;
  959. }
  960. }
  961. columns.Add(newBlocks);
  962. remainingBlocks = tempRemainingBlocks;
  963. }
  964. for(int i = 0; i < columns.Count; ++i)
  965. {
  966. foreach(var block in columns[i])
  967. {
  968. var nColumns = -1;
  969. for(int j = i + 1; j < columns.Count; ++j)
  970. {
  971. foreach(var block2 in columns[j])
  972. {
  973. if(block.StartTime < block2.EndTime && block.EndTime > block2.StartTime)
  974. {
  975. nColumns = j - i;
  976. break;
  977. }
  978. }
  979. if(nColumns > -1)
  980. {
  981. break;
  982. }
  983. }
  984. block.NColumns = nColumns > -1 ? nColumns : columns.Count - i;
  985. block.ColumnIndex = i;
  986. }
  987. }
  988. if(columns.Count == 0)
  989. {
  990. columns.Add(new());
  991. }
  992. return columns;
  993. }
  994. private void UpdateBlock(Block block)
  995. {
  996. Render(recalculatePositions: true);
  997. }
  998. }