|
|
@@ -0,0 +1,1162 @@
|
|
|
+using InABox.Core;
|
|
|
+using InABox.Wpf;
|
|
|
+using InABox.WPF;
|
|
|
+using Microsoft.CodeAnalysis.VisualBasic.Syntax;
|
|
|
+using NPOI.OpenXmlFormats.Spreadsheet;
|
|
|
+using System;
|
|
|
+using System.Collections;
|
|
|
+using System.Collections.Generic;
|
|
|
+using System.Collections.Specialized;
|
|
|
+using System.ComponentModel;
|
|
|
+using System.Diagnostics.CodeAnalysis;
|
|
|
+using System.Linq;
|
|
|
+using System.Reactive.Linq;
|
|
|
+using System.Text;
|
|
|
+using System.Threading;
|
|
|
+using System.Threading.Tasks;
|
|
|
+using System.Windows;
|
|
|
+using System.Windows.Controls;
|
|
|
+using System.Windows.Controls.Primitives;
|
|
|
+using System.Windows.Data;
|
|
|
+using System.Windows.Input;
|
|
|
+using System.Windows.Media;
|
|
|
+using System.Windows.Media.Animation;
|
|
|
+using System.Windows.Shapes;
|
|
|
+
|
|
|
+namespace InABox.WPF;
|
|
|
+
|
|
|
+public class CalendarBlockEventArgs(object? value, object column, DateTime date, TimeSpan start, TimeSpan end) : EventArgs
|
|
|
+{
|
|
|
+ public object? Value { get; set; } = value;
|
|
|
+ public DateTime Date { get; set; } = date;
|
|
|
+ public object Column { get; set; } = column;
|
|
|
+ public TimeSpan Start { get; set; } = start;
|
|
|
+ public TimeSpan End { get; set; } = end;
|
|
|
+}
|
|
|
+
|
|
|
+public class CalendarControl : ContentControl
|
|
|
+{
|
|
|
+ public static readonly DependencyProperty RowHeightProperty =
|
|
|
+ DependencyProperty.Register(nameof(RowHeight), typeof(double), typeof(CalendarControl), new(100.0, Render_Changed));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty MinimumColumnWidthProperty =
|
|
|
+ DependencyProperty.Register(nameof(MinimumColumnWidth), typeof(double), typeof(CalendarControl), new(50.0, Render_Changed));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty RowIntervalProperty =
|
|
|
+ DependencyProperty.Register(nameof(RowInterval), typeof(TimeSpan), typeof(CalendarControl), new(TimeSpan.FromHours(1), Render_Changed));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty ItemsSourceProperty =
|
|
|
+ DependencyProperty.Register(nameof(ItemsSource), typeof(IEnumerable), typeof(CalendarControl), new(ItemsSource_Changed));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty ItemTemplateProperty =
|
|
|
+ DependencyProperty.Register(nameof(ItemTemplate), typeof(DataTemplate), typeof(CalendarControl));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty DateTemplateProperty =
|
|
|
+ DependencyProperty.Register(nameof(DateTemplate), typeof(DataTemplate), typeof(CalendarControl));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty HeaderTemplateProperty =
|
|
|
+ DependencyProperty.Register(nameof(HeaderTemplate), typeof(DataTemplate), typeof(CalendarControl));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty ColumnsProperty =
|
|
|
+ DependencyProperty.Register(nameof(Columns), typeof(IEnumerable), typeof(CalendarControl), new(Columns_Changed));
|
|
|
+
|
|
|
+ public double MinimumColumnWidth
|
|
|
+ {
|
|
|
+ get => (double)GetValue(MinimumColumnWidthProperty);
|
|
|
+ set => SetValue(MinimumColumnWidthProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public double RowHeight
|
|
|
+ {
|
|
|
+ get => (double)GetValue(RowHeightProperty);
|
|
|
+ set => SetValue(RowHeightProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public TimeSpan RowInterval
|
|
|
+ {
|
|
|
+ get => (TimeSpan)GetValue(RowIntervalProperty);
|
|
|
+ set => SetValue(RowIntervalProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public BindingBase? DateMapping { get; set; }
|
|
|
+
|
|
|
+ public BindingBase? ColumnMapping { get; set; }
|
|
|
+
|
|
|
+ public BindingBase? StartTimeMapping { get; set; }
|
|
|
+
|
|
|
+ public BindingBase? EndTimeMapping { get; set; }
|
|
|
+
|
|
|
+ public IEnumerable? ItemsSource
|
|
|
+ {
|
|
|
+ get => GetValue(ItemsSourceProperty) as IEnumerable;
|
|
|
+ set => SetValue(ItemsSourceProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public DataTemplate? ItemTemplate
|
|
|
+ {
|
|
|
+ get => GetValue(ItemTemplateProperty) as DataTemplate;
|
|
|
+ set => SetValue(ItemTemplateProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public DataTemplate? DateTemplate
|
|
|
+ {
|
|
|
+ get => GetValue(DateTemplateProperty) as DataTemplate;
|
|
|
+ set => SetValue(DateTemplateProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public DataTemplate? HeaderTemplate
|
|
|
+ {
|
|
|
+ get => GetValue(HeaderTemplateProperty) as DataTemplate;
|
|
|
+ set => SetValue(HeaderTemplateProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public IEnumerable? Columns
|
|
|
+ {
|
|
|
+ get => GetValue(ColumnsProperty) as IEnumerable;
|
|
|
+ set => SetValue(ColumnsProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public event EventHandler<CalendarBlockEventArgs>? BlockClicked;
|
|
|
+ public event EventHandler<CalendarBlockEventArgs>? BlockHeld;
|
|
|
+
|
|
|
+ private static void Columns_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
+ {
|
|
|
+ if (d is not CalendarControl calendar) return;
|
|
|
+
|
|
|
+ if(e.OldValue is INotifyCollectionChanged oldNotify)
|
|
|
+ {
|
|
|
+ oldNotify.CollectionChanged -= calendar.ColumnsCollection_Changed;
|
|
|
+ }
|
|
|
+ calendar.Render(columnsChanged: true);
|
|
|
+ if(e.NewValue is INotifyCollectionChanged notify)
|
|
|
+ {
|
|
|
+ notify.CollectionChanged += calendar.ColumnsCollection_Changed;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ColumnsCollection_Changed(object? sender, NotifyCollectionChangedEventArgs e)
|
|
|
+ {
|
|
|
+ Render(columnsChanged: true);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void Render_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
+ {
|
|
|
+ if (d is not CalendarControl calendar) return;
|
|
|
+
|
|
|
+ calendar.Render();
|
|
|
+ }
|
|
|
+
|
|
|
+ private ScrollViewer DateScroll;
|
|
|
+ private ScrollViewer HeaderScroll;
|
|
|
+ private ScrollViewer LabelScroll;
|
|
|
+ private ScrollViewer MainScroll;
|
|
|
+
|
|
|
+ private ScrollBar VerticalScroll;
|
|
|
+ private ScrollBar HorizontalScroll;
|
|
|
+
|
|
|
+ private Canvas DateCanvas;
|
|
|
+ private Canvas HeaderCanvas;
|
|
|
+ private Canvas LabelCanvas;
|
|
|
+ private Canvas MainCanvas;
|
|
|
+
|
|
|
+ private Border HeaderBorder;
|
|
|
+
|
|
|
+ public CalendarControl()
|
|
|
+ {
|
|
|
+ var grid = new Grid();
|
|
|
+ grid.AddRow(GridUnitType.Auto); // Date
|
|
|
+ grid.AddRow(GridUnitType.Auto); // Column
|
|
|
+ grid.AddRow(GridUnitType.Star);
|
|
|
+ grid.AddRow(GridUnitType.Auto); // Scroll Bar
|
|
|
+
|
|
|
+ grid.AddColumn(GridUnitType.Auto); // Times
|
|
|
+ grid.AddColumn(GridUnitType.Star);
|
|
|
+ grid.AddColumn(GridUnitType.Auto); // ScrollBar
|
|
|
+
|
|
|
+ DateScroll = new ScrollViewer
|
|
|
+ {
|
|
|
+ HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
|
|
|
+ VerticalScrollBarVisibility = ScrollBarVisibility.Disabled
|
|
|
+ };
|
|
|
+ DateCanvas = new Canvas
|
|
|
+ {
|
|
|
+
|
|
|
+ };
|
|
|
+ DateScroll.Content = DateCanvas;
|
|
|
+
|
|
|
+ HeaderScroll = new ScrollViewer
|
|
|
+ {
|
|
|
+ HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
|
|
|
+ VerticalScrollBarVisibility = ScrollBarVisibility.Disabled
|
|
|
+ };
|
|
|
+ HeaderCanvas = new Canvas
|
|
|
+ {
|
|
|
+
|
|
|
+ };
|
|
|
+ HeaderScroll.Content = HeaderCanvas;
|
|
|
+
|
|
|
+ LabelScroll = new ScrollViewer
|
|
|
+ {
|
|
|
+ VerticalScrollBarVisibility = ScrollBarVisibility.Hidden,
|
|
|
+ HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled
|
|
|
+ };
|
|
|
+ LabelCanvas = new Canvas
|
|
|
+ {
|
|
|
+ Margin = new(2, 0, 2, 0)
|
|
|
+ };
|
|
|
+ LabelScroll.Content = LabelCanvas;
|
|
|
+
|
|
|
+ MainScroll = new ScrollViewer
|
|
|
+ {
|
|
|
+ HorizontalScrollBarVisibility = ScrollBarVisibility.Hidden,
|
|
|
+ VerticalScrollBarVisibility = ScrollBarVisibility.Hidden
|
|
|
+ };
|
|
|
+ MainCanvas = new Canvas
|
|
|
+ {
|
|
|
+
|
|
|
+ };
|
|
|
+ MainScroll.Content = MainCanvas;
|
|
|
+ MainScroll.SizeChanged += MainScroll_SizeChanged;
|
|
|
+ MainScroll.ScrollChanged += MainScroll_ScrollChanged;
|
|
|
+ MainScroll.PreviewMouseWheel += MainScroll_PreviewMouseWheel;
|
|
|
+
|
|
|
+ grid.AddChild(
|
|
|
+ new Border
|
|
|
+ {
|
|
|
+ BorderBrush = Colors.LightGray.ToBrush(),
|
|
|
+ BorderThickness = new(0, 0, 1, 0)
|
|
|
+ },
|
|
|
+ row: 0, rowSpan: 2,
|
|
|
+ column: 0);
|
|
|
+ grid.AddChild(
|
|
|
+ new Border
|
|
|
+ {
|
|
|
+ BorderBrush = Colors.LightGray.ToBrush(),
|
|
|
+ BorderThickness = new(0, 0, 0, 1),
|
|
|
+ Child = DateScroll
|
|
|
+ },
|
|
|
+ row: 0,
|
|
|
+ column: 1, colSpan: 2);
|
|
|
+ HeaderBorder = new Border
|
|
|
+ {
|
|
|
+ BorderBrush = Colors.LightGray.ToBrush(),
|
|
|
+ BorderThickness = new(0, 0, 0, 1),
|
|
|
+ Child = HeaderScroll
|
|
|
+ };
|
|
|
+ grid.AddChild(
|
|
|
+ HeaderBorder,
|
|
|
+ row: 1,
|
|
|
+ column: 1, colSpan: 2);
|
|
|
+ grid.AddChild(
|
|
|
+ new Border
|
|
|
+ {
|
|
|
+ BorderBrush = Colors.LightGray.ToBrush(),
|
|
|
+ BorderThickness = new(0, 0, 1, 0),
|
|
|
+ Child = LabelScroll
|
|
|
+ },
|
|
|
+ row: 2, rowSpan: 2,
|
|
|
+ column: 0);
|
|
|
+ grid.AddChild(
|
|
|
+ new Border
|
|
|
+ {
|
|
|
+ Child = MainScroll
|
|
|
+ },
|
|
|
+ row: 2, rowSpan: 2,
|
|
|
+ column: 1, colSpan: 2);
|
|
|
+
|
|
|
+ VerticalScroll = new ScrollBar();
|
|
|
+ VerticalScroll.Scroll += VerticalScroll_Scroll;
|
|
|
+ VerticalScroll.Opacity = 0.5;
|
|
|
+ VerticalScroll.MouseEnter += Scroll_MouseEnter;
|
|
|
+ VerticalScroll.MouseLeave += Scroll_MouseLeave;
|
|
|
+
|
|
|
+ HorizontalScroll = new ScrollBar
|
|
|
+ {
|
|
|
+ Orientation = Orientation.Horizontal
|
|
|
+ };
|
|
|
+ HorizontalScroll.Scroll += HorizontalScroll_Scroll;
|
|
|
+ HorizontalScroll.Opacity = 0.5;
|
|
|
+ HorizontalScroll.MouseEnter += Scroll_MouseEnter;
|
|
|
+ HorizontalScroll.MouseLeave += Scroll_MouseLeave;
|
|
|
+
|
|
|
+ grid.AddChild(VerticalScroll, 2, 2);
|
|
|
+ grid.AddChild(HorizontalScroll, 3, 1);
|
|
|
+
|
|
|
+ Content = grid;
|
|
|
+ }
|
|
|
+
|
|
|
+ private (double x, double y) _currentScroll;
|
|
|
+
|
|
|
+ private void Scroll_MouseLeave(object sender, MouseEventArgs e)
|
|
|
+ {
|
|
|
+ if(sender is ScrollBar bar)
|
|
|
+ {
|
|
|
+ bar.Opacity = 0.5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Scroll_MouseEnter(object sender, MouseEventArgs e)
|
|
|
+ {
|
|
|
+ if(sender is ScrollBar bar)
|
|
|
+ {
|
|
|
+ bar.Opacity = 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void MainScroll_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
|
|
|
+ {
|
|
|
+ if (Keyboard.Modifiers.HasFlag(ModifierKeys.Shift))
|
|
|
+ {
|
|
|
+ MainScroll.ScrollToHorizontalOffset(MainScroll.HorizontalOffset - e.Delta);
|
|
|
+ e.Handled = true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void HorizontalScroll_Scroll(object sender, ScrollEventArgs e)
|
|
|
+ {
|
|
|
+ MainScroll.ScrollToHorizontalOffset(e.NewValue);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void VerticalScroll_Scroll(object sender, ScrollEventArgs e)
|
|
|
+ {
|
|
|
+ MainScroll.ScrollToVerticalOffset(e.NewValue);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void MainScroll_ScrollChanged(object sender, ScrollChangedEventArgs e)
|
|
|
+ {
|
|
|
+ LabelScroll.ScrollToVerticalOffset(e.VerticalOffset);
|
|
|
+ HeaderScroll.ScrollToHorizontalOffset(e.HorizontalOffset);
|
|
|
+ DateScroll.ScrollToHorizontalOffset(e.HorizontalOffset);
|
|
|
+ HorizontalScroll.Value = e.HorizontalOffset;
|
|
|
+ VerticalScroll.Value = e.VerticalOffset;
|
|
|
+ _currentScroll = (e.HorizontalOffset, e.VerticalOffset);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static void ItemsSource_Changed(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
+ {
|
|
|
+ if (d is not CalendarControl calendar) return;
|
|
|
+
|
|
|
+ if(e.OldValue is INotifyCollectionChanged oldNotify)
|
|
|
+ {
|
|
|
+ oldNotify.CollectionChanged -= calendar.Collection_Changed;
|
|
|
+ }
|
|
|
+ calendar.Render(itemsChanged: true);
|
|
|
+ if(calendar.ItemsSource is INotifyCollectionChanged notify)
|
|
|
+ {
|
|
|
+ notify.CollectionChanged += calendar.Collection_Changed;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Collection_Changed(object? sender, NotifyCollectionChangedEventArgs e)
|
|
|
+ {
|
|
|
+ Render(itemsChanged: true);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void MainScroll_SizeChanged(object sender, SizeChangedEventArgs e)
|
|
|
+ {
|
|
|
+ Render();
|
|
|
+ }
|
|
|
+
|
|
|
+ private class Block : Border, INotifyPropertyChanged
|
|
|
+ {
|
|
|
+ public static readonly DependencyProperty ColumnProperty =
|
|
|
+ DependencyProperty.Register(nameof(Column), typeof(object), typeof(Block), new(OnPropertyChangedHandler));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty DateProperty =
|
|
|
+ DependencyProperty.Register(nameof(Date), typeof(DateTime), typeof(Block), new(OnPropertyChangedHandler));
|
|
|
+
|
|
|
+ public static readonly DependencyProperty StartTimeProperty =
|
|
|
+ DependencyProperty.Register(nameof(StartTime), typeof(TimeSpan), typeof(Block));
|
|
|
+ public static readonly DependencyProperty EndTimeProperty =
|
|
|
+ DependencyProperty.Register(nameof(EndTime), typeof(TimeSpan), typeof(Block));
|
|
|
+
|
|
|
+ public int ColumnIndex { get; set; } = -1;
|
|
|
+
|
|
|
+ public int NColumns { get; set; } = -1;
|
|
|
+
|
|
|
+ public object Column
|
|
|
+ {
|
|
|
+ get => GetValue(ColumnProperty);
|
|
|
+ set => SetValue(ColumnProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public DateTime Date
|
|
|
+ {
|
|
|
+ get => (DateTime)GetValue(DateProperty);
|
|
|
+ set => SetValue(DateProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public TimeSpan StartTime
|
|
|
+ {
|
|
|
+ get => (TimeSpan)GetValue(StartTimeProperty);
|
|
|
+ set => SetValue(StartTimeProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ public TimeSpan EndTime
|
|
|
+ {
|
|
|
+ get => (TimeSpan)GetValue(EndTimeProperty);
|
|
|
+ set => SetValue(EndTimeProperty, value);
|
|
|
+ }
|
|
|
+
|
|
|
+ private ContentControl _contentControl;
|
|
|
+ public ContentControl ContentControl => _contentControl;
|
|
|
+
|
|
|
+ public object? Content => _contentControl.Content;
|
|
|
+
|
|
|
+ public override string ToString()
|
|
|
+ {
|
|
|
+ return $"Block({Column}: {StartTime:hh\\:mm} - {EndTime:hh\\:mm})";
|
|
|
+ }
|
|
|
+
|
|
|
+ public Block(CalendarControl parent, object content)
|
|
|
+ {
|
|
|
+ _contentControl = new ContentControl
|
|
|
+ {
|
|
|
+ Content = content
|
|
|
+ };
|
|
|
+ _contentControl.Bind(ContentControl.ContentTemplateProperty, parent, x => x.ItemTemplate);
|
|
|
+ Child = _contentControl;
|
|
|
+ }
|
|
|
+
|
|
|
+ public event PropertyChangedEventHandler? PropertyChanged;
|
|
|
+
|
|
|
+ private static void OnPropertyChangedHandler(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
|
|
+ {
|
|
|
+ if (d is not Block block) return;
|
|
|
+ block.PropertyChanged?.Invoke(block, new(e.Property.Name));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private class Column
|
|
|
+ {
|
|
|
+ public List<Block> Blocks { get; set; } = new();
|
|
|
+
|
|
|
+ public List<List<Block>>? Columns { get; set; } = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private List<Block> _blockList = new();
|
|
|
+ private Dictionary<DateTime, Dictionary<object, Column>> _blocks = new();
|
|
|
+
|
|
|
+ private List<IDisposable> _oldSubscriptions = new();
|
|
|
+
|
|
|
+ private List<object> _columns = new();
|
|
|
+ private List<(DateTime, object)> _columnList = new();
|
|
|
+
|
|
|
+ private IEnumerable<KeyValuePair<(DateTime, object), Column>> _allColumns =>
|
|
|
+ _blocks.SelectMany(x => x.Value.Select(y => new KeyValuePair<(DateTime, object), Column>((x.Key, y.Key), y.Value)));
|
|
|
+
|
|
|
+ private class ActionDisposable(Action onDispose) : IDisposable
|
|
|
+ {
|
|
|
+ public void Dispose()
|
|
|
+ {
|
|
|
+ onDispose();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool RecreateBlocksList()
|
|
|
+ {
|
|
|
+ if (ItemsSource is null) return false;
|
|
|
+
|
|
|
+ var columnBinding = ColumnMapping;
|
|
|
+ var dateBinding = DateMapping;
|
|
|
+ var startBinding = StartTimeMapping;
|
|
|
+ var endBinding = EndTimeMapping;
|
|
|
+ if(columnBinding is null || dateBinding is null || startBinding is null || endBinding is null)
|
|
|
+ {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach(var subscription in _oldSubscriptions)
|
|
|
+ {
|
|
|
+ subscription.Dispose();
|
|
|
+ }
|
|
|
+ _oldSubscriptions.Clear();
|
|
|
+ _blockList.Clear();
|
|
|
+ foreach(var item in ItemsSource)
|
|
|
+ {
|
|
|
+ if (item is null) continue;
|
|
|
+ var block = new Block(this, item);
|
|
|
+ block.SetBinding(Block.ColumnProperty, columnBinding);
|
|
|
+ block.SetBinding(Block.StartTimeProperty, startBinding);
|
|
|
+ block.SetBinding(Block.EndTimeProperty, endBinding);
|
|
|
+ block.SetBinding(Block.DateProperty, dateBinding);
|
|
|
+ block.DataContext = item;
|
|
|
+ block.Background = Colors.Transparent.ToBrush();
|
|
|
+
|
|
|
+ block.PropertyChanged += Block_PropertyChanged;
|
|
|
+ _oldSubscriptions.Add(new ActionDisposable(() => block.PropertyChanged -= Block_PropertyChanged));
|
|
|
+
|
|
|
+ block.ContentControl.MouseLeftButtonDown += (o, e) =>
|
|
|
+ {
|
|
|
+ Block_MouseLeftButtonDown(block, e);
|
|
|
+ };
|
|
|
+ block.ContentControl.MouseLeftButtonUp += (o, e) =>
|
|
|
+ {
|
|
|
+ Block_MouseLeftButtonUp(block, e);
|
|
|
+ };
|
|
|
+
|
|
|
+ _blockList.Add(block);
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void RefreshColumns(bool itemsChanged = false)
|
|
|
+ {
|
|
|
+ if (itemsChanged)
|
|
|
+ {
|
|
|
+ if (!RecreateBlocksList()) return;
|
|
|
+ }
|
|
|
+
|
|
|
+ _blocks.Clear();
|
|
|
+
|
|
|
+ _columns.Clear();
|
|
|
+ var autoGenerateColumns = true;
|
|
|
+ if(Columns is not null)
|
|
|
+ {
|
|
|
+ autoGenerateColumns = false;
|
|
|
+ foreach(var column in Columns)
|
|
|
+ {
|
|
|
+ if(column is null) continue;
|
|
|
+ _columns.Add(column);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ foreach(var block in _blockList)
|
|
|
+ {
|
|
|
+ var column = block.Column;
|
|
|
+ var date = block.Date;
|
|
|
+ if(column is null)
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ if(!_blocks.TryGetValue(date, out var dateBlocks))
|
|
|
+ {
|
|
|
+ dateBlocks = new();
|
|
|
+ _blocks.Add(date, dateBlocks);
|
|
|
+ if (!autoGenerateColumns)
|
|
|
+ {
|
|
|
+ foreach(var col in _columns)
|
|
|
+ {
|
|
|
+ if(!dateBlocks.TryAdd(col, new()))
|
|
|
+ {
|
|
|
+ throw new Exception($"Duplicate column {column} in Calendar");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if(!dateBlocks.TryGetValue(column, out var columnBlocks))
|
|
|
+ {
|
|
|
+ if (!autoGenerateColumns) continue;
|
|
|
+
|
|
|
+ columnBlocks = new();
|
|
|
+ dateBlocks.Add(column, columnBlocks);
|
|
|
+ _columns.Add(column);
|
|
|
+ }
|
|
|
+
|
|
|
+ columnBlocks.Blocks.Add(block);
|
|
|
+ }
|
|
|
+ HeaderBorder.Visibility = _columns.Count <= 1 ? Visibility.Collapsed : Visibility.Visible;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Block_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
|
|
+ {
|
|
|
+ if (sender is not Block block) return;
|
|
|
+
|
|
|
+ if(e.PropertyName == nameof(Block.Column))
|
|
|
+ {
|
|
|
+ Render(columnsChanged: true);
|
|
|
+ }
|
|
|
+ else if(e.PropertyName == nameof(Block.StartTime)
|
|
|
+ || e.PropertyName == nameof(Block.EndTime))
|
|
|
+ {
|
|
|
+ UpdateBlock(block);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private double _colWidth;
|
|
|
+ private double _colSpace;
|
|
|
+ private double _rowHeight;
|
|
|
+
|
|
|
+ private bool _columnsChanged = false;
|
|
|
+ private bool _itemsChanged = false;
|
|
|
+ private bool _recalculatePositions = false;
|
|
|
+ private bool _rerendering = false;
|
|
|
+
|
|
|
+ private void Render(bool columnsChanged = false, bool itemsChanged = false, bool recalculatePositions = false)
|
|
|
+ {
|
|
|
+ _columnsChanged = _columnsChanged || columnsChanged;
|
|
|
+ _itemsChanged = _itemsChanged || itemsChanged;
|
|
|
+ _recalculatePositions = _recalculatePositions || recalculatePositions;
|
|
|
+ if (!_rerendering)
|
|
|
+ {
|
|
|
+ _rerendering = true;
|
|
|
+ Dispatcher.BeginInvoke(DoRender);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private double _lastColWidth;
|
|
|
+
|
|
|
+ private void DoRender()
|
|
|
+ {
|
|
|
+ _rerendering = false;
|
|
|
+
|
|
|
+ var itemsChanged = _itemsChanged;
|
|
|
+ var columnsChanged = _columnsChanged;
|
|
|
+ var recalculatePositions = _recalculatePositions;
|
|
|
+ _columnsChanged = false;
|
|
|
+ _itemsChanged = false;
|
|
|
+ _recalculatePositions = false;
|
|
|
+ if (itemsChanged || columnsChanged)
|
|
|
+ {
|
|
|
+ RefreshColumns(itemsChanged: itemsChanged);
|
|
|
+ }
|
|
|
+ if (recalculatePositions)
|
|
|
+ {
|
|
|
+ foreach(var (column, columnBlocks) in _allColumns)
|
|
|
+ {
|
|
|
+ columnBlocks.Columns = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ var nRows = (24 / RowInterval.TotalHours);
|
|
|
+ var rowHeight = Math.Max(RowHeight, MainScroll.ActualHeight / nRows);
|
|
|
+
|
|
|
+ MainCanvas.Children.Clear();
|
|
|
+ MainCanvas.Height = rowHeight * nRows;
|
|
|
+
|
|
|
+ var minColWidth = MinimumColumnWidth;
|
|
|
+ var colSpace = 1;
|
|
|
+
|
|
|
+ var nColumns = 0;
|
|
|
+ foreach (var (column, columnBlocks) in _allColumns)
|
|
|
+ {
|
|
|
+ columnBlocks.Columns ??= RecalculateBlockPositionsForDay(columnBlocks.Blocks);
|
|
|
+ // columnsPerDay = Math.Max(columnsPerDay, columnBlocks.Columns.Count);
|
|
|
+ nColumns += columnBlocks.Columns.Count;
|
|
|
+ }
|
|
|
+ // nColumns = columnsPerDay * _blocks.Count
|
|
|
+
|
|
|
+ var colWidth = (Math.Max((MainScroll.ActualWidth - colSpace * (_blocks.Sum(x => x.Value.Count) - 1)) / nColumns, minColWidth));
|
|
|
+ var lastColWidth = _lastColWidth;
|
|
|
+ _lastColWidth = colWidth;
|
|
|
+
|
|
|
+ var updateHeaders = colWidth != lastColWidth || columnsChanged || itemsChanged;
|
|
|
+
|
|
|
+ if (updateHeaders)
|
|
|
+ {
|
|
|
+ HeaderCanvas.Children.Clear();
|
|
|
+ DateCanvas.Children.Clear();
|
|
|
+ }
|
|
|
+
|
|
|
+ ClearHeldSelection();
|
|
|
+
|
|
|
+ _rowHeight = rowHeight;
|
|
|
+ _colWidth = colWidth;
|
|
|
+ _colSpace = colSpace;
|
|
|
+
|
|
|
+ var minY = double.MaxValue;
|
|
|
+
|
|
|
+ var colX = 0.0;
|
|
|
+
|
|
|
+ var dates = _blocks.Keys.ToArray();
|
|
|
+ Array.Sort(dates);
|
|
|
+
|
|
|
+ _columnList.Clear();
|
|
|
+ var columnIdx = 0;
|
|
|
+ foreach(var date in dates)
|
|
|
+ {
|
|
|
+ if (!_blocks.TryGetValue(date, out var dateBlocks)) continue;
|
|
|
+
|
|
|
+ if (updateHeaders)
|
|
|
+ {
|
|
|
+ var nDateColumns = dateBlocks.Sum(x => x.Value.Columns!.Count);
|
|
|
+ var dateHeader = new ContentControl
|
|
|
+ {
|
|
|
+ Content = date,
|
|
|
+ Width = colWidth * nDateColumns + colSpace * (nDateColumns - 1)
|
|
|
+ };
|
|
|
+ dateHeader.Bind(ContentControl.ContentTemplateProperty, this, x => x.DateTemplate);
|
|
|
+ Canvas.SetLeft(dateHeader, colX);
|
|
|
+ DateCanvas.Children.Add(dateHeader);
|
|
|
+ dateHeader.SizeChanged += DateHeader_SizeChanged;
|
|
|
+ }
|
|
|
+
|
|
|
+ var dateColumnIndex = 0;
|
|
|
+ foreach(var columnKey in _columns)
|
|
|
+ {
|
|
|
+ if(!dateBlocks.TryGetValue(columnKey, out var columnBlocks))
|
|
|
+ {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ _columnList.Add((date, columnKey));
|
|
|
+
|
|
|
+ if (updateHeaders)
|
|
|
+ {
|
|
|
+ var columnHeader = new ContentControl
|
|
|
+ {
|
|
|
+ Content = columnKey,
|
|
|
+ Width = colWidth * columnBlocks.Columns!.Count,
|
|
|
+ };
|
|
|
+ columnHeader.Bind(ContentControl.ContentTemplateProperty, this, x => x.HeaderTemplate);
|
|
|
+ Canvas.SetLeft(columnHeader, colX);
|
|
|
+ HeaderCanvas.Children.Add(columnHeader);
|
|
|
+ columnHeader.SizeChanged += ColumnHeader_SizeChanged;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add cell placeholders
|
|
|
+ var rowIdx = 0;
|
|
|
+ for(var time = TimeSpan.Zero; time < TimeSpan.FromHours(24); time += RowInterval)
|
|
|
+ {
|
|
|
+ var rectangle = new Rectangle
|
|
|
+ {
|
|
|
+ Width = colWidth * columnBlocks.Columns!.Count,
|
|
|
+ Height = RowHeight,
|
|
|
+ Fill = new SolidColorBrush(Colors.Transparent),
|
|
|
+ };
|
|
|
+ rectangle.MouseEnter += (o, e) =>
|
|
|
+ {
|
|
|
+ rectangle.Fill = Colors.LightBlue.ToBrush();
|
|
|
+ };
|
|
|
+ rectangle.MouseLeave += (o, e) =>
|
|
|
+ {
|
|
|
+ rectangle.Fill = Colors.Transparent.ToBrush();
|
|
|
+ };
|
|
|
+ rectangle.MouseLeftButtonDown += Rectangle_MouseLeftButtonDown;
|
|
|
+ rectangle.MouseLeftButtonUp += Rectangle_MouseLeftButtonUp;
|
|
|
+ Canvas.SetLeft(rectangle, colX);
|
|
|
+ Canvas.SetTop(rectangle, rowIdx * rowHeight);
|
|
|
+ MainCanvas.Children.Add(rectangle);
|
|
|
+
|
|
|
+ ++rowIdx;
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+ MainCanvas.Children.Add(block);
|
|
|
+ }
|
|
|
+
|
|
|
+ colX += colWidth;
|
|
|
+ }
|
|
|
+
|
|
|
+ // Add Header separators
|
|
|
+ if(columnIdx < nColumns - 1)
|
|
|
+ {
|
|
|
+ var rectangle = new Rectangle
|
|
|
+ {
|
|
|
+ Width = 0.75,
|
|
|
+ Height = MainCanvas.Height,
|
|
|
+ Fill = new SolidColorBrush(Colors.LightGray)
|
|
|
+ };
|
|
|
+ Canvas.SetLeft(rectangle, colX);
|
|
|
+ MainCanvas.Children.Add(rectangle);
|
|
|
+
|
|
|
+ if (updateHeaders)
|
|
|
+ {
|
|
|
+ var headRectangle = new Rectangle
|
|
|
+ {
|
|
|
+ Width = 0.75,
|
|
|
+ Fill = new SolidColorBrush(Colors.LightGray)
|
|
|
+ };
|
|
|
+ headRectangle.Bind(Rectangle.HeightProperty, HeaderCanvas, x => x.ActualHeight);
|
|
|
+ Canvas.SetLeft(headRectangle, colX);
|
|
|
+ HeaderCanvas.Children.Add(headRectangle);
|
|
|
+
|
|
|
+ if(dateColumnIndex == dateBlocks.Count - 1)
|
|
|
+ {
|
|
|
+ var dateRectangle = new Rectangle
|
|
|
+ {
|
|
|
+ Width = 0.75,
|
|
|
+ Fill = new SolidColorBrush(Colors.LightGray)
|
|
|
+ };
|
|
|
+ dateRectangle.Bind(Rectangle.HeightProperty, DateCanvas, x => x.ActualHeight);
|
|
|
+ Canvas.SetLeft(dateRectangle, colX);
|
|
|
+ DateCanvas.Children.Add(dateRectangle);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ colX += colSpace;
|
|
|
+ }
|
|
|
+ ++dateColumnIndex;
|
|
|
+ ++columnIdx;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ MainCanvas.Width = Math.Floor(colX);
|
|
|
+ HeaderCanvas.Width = Math.Floor(colX);
|
|
|
+ DateCanvas.Width = Math.Floor(colX);
|
|
|
+
|
|
|
+ VerticalScroll.Minimum = 0;
|
|
|
+ VerticalScroll.Maximum = MainCanvas.Height - MainScroll.ActualHeight;
|
|
|
+ VerticalScroll.ViewportSize = MainScroll.ActualHeight;
|
|
|
+ VerticalScroll.Visibility = VerticalScroll.Maximum < 1 ? Visibility.Collapsed : Visibility.Visible;
|
|
|
+
|
|
|
+ HorizontalScroll.Minimum = 0;
|
|
|
+ HorizontalScroll.Maximum = MainCanvas.Width - MainScroll.ActualWidth;
|
|
|
+ HorizontalScroll.ViewportSize = MainScroll.ActualWidth;
|
|
|
+ HorizontalScroll.Visibility = HorizontalScroll.Maximum < 1 ? Visibility.Collapsed : Visibility.Visible;
|
|
|
+
|
|
|
+ if(minY == double.MaxValue)
|
|
|
+ {
|
|
|
+ MainScroll.ScrollToHorizontalOffset(_currentScroll.x);
|
|
|
+ MainScroll.ScrollToVerticalOffset(_currentScroll.y);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ MainScroll.ScrollToHorizontalOffset(_currentScroll.x);
|
|
|
+ MainScroll.ScrollToVerticalOffset(Math.Max(minY - RowHeight / 2, _currentScroll.y));
|
|
|
+ }
|
|
|
+
|
|
|
+ var lines = new List<FrameworkElement>();
|
|
|
+
|
|
|
+ LabelCanvas.Children.Clear();
|
|
|
+ LabelCanvas.Height = MainCanvas.Height;
|
|
|
+
|
|
|
+ var y = rowHeight;
|
|
|
+ for(var time = RowInterval; time < TimeSpan.FromHours(24); time += RowInterval)
|
|
|
+ {
|
|
|
+ var rectangle = new Rectangle
|
|
|
+ {
|
|
|
+ Width = MainCanvas.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),
|
|
|
+ FontSize = 10
|
|
|
+ };
|
|
|
+ block.SizeChanged += Block_SizeChanged;
|
|
|
+ Canvas.SetTop(block, y);
|
|
|
+ LabelCanvas.Children.Add(block);
|
|
|
+
|
|
|
+ y += rowHeight;
|
|
|
+ }
|
|
|
+
|
|
|
+ for(var i = 0; i < lines.Count; ++i)
|
|
|
+ {
|
|
|
+ MainCanvas.Children.Insert(i, lines[i]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private bool TryGetBlockFromPosition(MouseEventArgs e, out DateTime blockDate, [NotNullWhen(true)] out object? column, out TimeSpan start, out TimeSpan end, out int index)
|
|
|
+ {
|
|
|
+ var point = e.GetPosition(MainCanvas);
|
|
|
+ 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;
|
|
|
+ index = -1;
|
|
|
+ blockDate = DateTime.MinValue;
|
|
|
+ var x = point.X;
|
|
|
+ foreach(var (date, columnKey) in _columnList)
|
|
|
+ {
|
|
|
+ if (!_blocks.TryGetValue(date, out var dateBlocks)
|
|
|
+ || !dateBlocks.TryGetValue(columnKey, out var columnBlocks)) continue;
|
|
|
+
|
|
|
+ var colWidth = columnBlocks.Columns!.Count * _colWidth + _colSpace;
|
|
|
+
|
|
|
+ if(x < colWidth)
|
|
|
+ {
|
|
|
+ column = columnKey;
|
|
|
+ index = Math.Min((int)Math.Floor(x / _colWidth), columnBlocks.Columns!.Count - 1);
|
|
|
+ blockDate = date;
|
|
|
+ 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_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
|
+ {
|
|
|
+ if (sender is not Block block) return;
|
|
|
+ e.Handled = true;
|
|
|
+
|
|
|
+ PressedAction(() => BlockHeld?.Invoke(this, new(block.Content, block.Column, block.Date, block.StartTime, block.EndTime)));
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Block_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
|
|
|
+ {
|
|
|
+ if (sender is not Block block) return;
|
|
|
+ e.Handled = true;
|
|
|
+
|
|
|
+ ReleasedAction(() => BlockClicked?.Invoke(this, new(block.Content, block.Column, block.Date, block.StartTime, block.EndTime)));
|
|
|
+ }
|
|
|
+
|
|
|
+ private Border? _heldSelection;
|
|
|
+
|
|
|
+ public void ClearHeldSelection()
|
|
|
+ {
|
|
|
+ MainCanvas.Children.Remove(_heldSelection);
|
|
|
+ _heldSelection = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Rectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
|
|
|
+ {
|
|
|
+ if (sender is not UIElement element) return;
|
|
|
+ if (!TryGetBlockFromPosition(e, out var date, out var column, out var start, out var end, out var index)) return;
|
|
|
+
|
|
|
+ var pos = e.GetPosition(MainCanvas);
|
|
|
+ ClearHeldSelection();
|
|
|
+
|
|
|
+ PressedAction(() =>
|
|
|
+ {
|
|
|
+ if(_heldSelection is null)
|
|
|
+ {
|
|
|
+ _heldSelection = new Border
|
|
|
+ {
|
|
|
+ Background = Colors.Black.ToBrush(0.2)
|
|
|
+ };
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ MainCanvas.Children.Remove(_heldSelection);
|
|
|
+ }
|
|
|
+
|
|
|
+ var element = MainCanvas.Children.OfType<Block>().FirstOrDefault(x => x.Date == date && x.Column == column);
|
|
|
+ if(element is not null)
|
|
|
+ {
|
|
|
+ var idx = MainCanvas.Children.IndexOf(element);
|
|
|
+ MainCanvas.Children.Insert(idx + 1, _heldSelection);
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ MainCanvas.Children.Add(_heldSelection);
|
|
|
+ }
|
|
|
+
|
|
|
+ var blockStart = start;
|
|
|
+ var blockEnd = TimeSpan.MinValue;
|
|
|
+
|
|
|
+ var x = 0.0;
|
|
|
+ foreach(var (columnDate, columnKey) in _columnList)
|
|
|
+ {
|
|
|
+ if (!_blocks.TryGetValue(columnDate, out var dateBlocks)
|
|
|
+ || !dateBlocks.TryGetValue(columnKey, out var columnBlocks)) continue;
|
|
|
+
|
|
|
+ var colWidth = columnBlocks.Columns!.Count * _colWidth + _colSpace;
|
|
|
+ if(date == columnDate && column == columnKey)
|
|
|
+ {
|
|
|
+ x += index * _colWidth;
|
|
|
+
|
|
|
+ var list = columnBlocks.Blocks.Where(x =>
|
|
|
+ {
|
|
|
+ return x.ColumnIndex <= index && index < x.ColumnIndex + x.NColumns;
|
|
|
+ }).ToList();
|
|
|
+ list.SortBy(x => x.StartTime);
|
|
|
+ for(int i = 0; i < list.Count; ++i)
|
|
|
+ {
|
|
|
+ if(start < list[i].StartTime)
|
|
|
+ {
|
|
|
+ blockEnd = list[i].StartTime;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ else
|
|
|
+ {
|
|
|
+ blockStart = list[i].EndTime;
|
|
|
+ if(i == list.Count - 1)
|
|
|
+ {
|
|
|
+ blockEnd = TimeSpan.FromHours(24).Subtract(TimeSpan.FromTicks(1));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ x += colWidth;
|
|
|
+ }
|
|
|
+
|
|
|
+ if(blockEnd == TimeSpan.MinValue)
|
|
|
+ {
|
|
|
+ blockStart = start;
|
|
|
+ blockEnd = end;
|
|
|
+ }
|
|
|
+ var top = (blockStart.TotalHours / RowInterval.TotalHours) * RowHeight;
|
|
|
+ var height = ((blockEnd - blockStart).TotalHours / RowInterval.TotalHours) * RowHeight;
|
|
|
+
|
|
|
+ var width = _colWidth;
|
|
|
+
|
|
|
+ var duration = TimeSpan.FromSeconds(0.2);
|
|
|
+
|
|
|
+ var leftAnimation = new DoubleAnimation
|
|
|
+ {
|
|
|
+ From = pos.X,
|
|
|
+ To = x,
|
|
|
+ Duration = duration
|
|
|
+ };
|
|
|
+ Storyboard.SetTarget(leftAnimation, _heldSelection);
|
|
|
+ Storyboard.SetTargetProperty(leftAnimation, new PropertyPath("(Canvas.Left)"));
|
|
|
+ var widthAnimation = new DoubleAnimation
|
|
|
+ {
|
|
|
+ From = 0,
|
|
|
+ To = width,
|
|
|
+ Duration = duration
|
|
|
+ };
|
|
|
+ Storyboard.SetTarget(widthAnimation, _heldSelection);
|
|
|
+ Storyboard.SetTargetProperty(widthAnimation, new PropertyPath("Width"));
|
|
|
+ var topAnimation = new DoubleAnimation
|
|
|
+ {
|
|
|
+ From = pos.Y,
|
|
|
+ To = top,
|
|
|
+ Duration = duration
|
|
|
+ };
|
|
|
+ Storyboard.SetTarget(topAnimation, _heldSelection);
|
|
|
+ Storyboard.SetTargetProperty(topAnimation, new PropertyPath("(Canvas.Top)"));
|
|
|
+ var heightAnimation = new DoubleAnimation
|
|
|
+ {
|
|
|
+ From = 0,
|
|
|
+ To = height,
|
|
|
+ Duration = duration
|
|
|
+ };
|
|
|
+ Storyboard.SetTarget(heightAnimation, _heldSelection);
|
|
|
+ Storyboard.SetTargetProperty(heightAnimation, new PropertyPath("Height"));
|
|
|
+ var storyBoard = new Storyboard();
|
|
|
+ storyBoard.Children.Add(leftAnimation);
|
|
|
+ storyBoard.Children.Add(topAnimation);
|
|
|
+ storyBoard.Children.Add(widthAnimation);
|
|
|
+ storyBoard.Children.Add(heightAnimation);
|
|
|
+ storyBoard.Completed += (o, e) =>
|
|
|
+ {
|
|
|
+ BlockHeld?.Invoke(this, new(null, column, date, blockStart, blockEnd));
|
|
|
+ };
|
|
|
+ storyBoard.Begin();
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Rectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
|
|
|
+ {
|
|
|
+ if (!TryGetBlockFromPosition(e, out var date, out var column, out var start, out var end, out var index)) return;
|
|
|
+ ReleasedAction(() =>
|
|
|
+ {
|
|
|
+ BlockClicked?.Invoke(this, new(null, column, date, start, end));
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private void ColumnHeader_SizeChanged(object? sender, SizeChangedEventArgs e)
|
|
|
+ {
|
|
|
+ HeaderCanvas.Height = HeaderCanvas.Children.OfType<FrameworkElement>().Select(x => x.ActualHeight).Max();
|
|
|
+ }
|
|
|
+ private void DateHeader_SizeChanged(object sender, SizeChangedEventArgs e)
|
|
|
+ {
|
|
|
+ DateCanvas.Height = DateCanvas.Children.OfType<FrameworkElement>().Select(x => x.ActualHeight).Max();
|
|
|
+ }
|
|
|
+
|
|
|
+ private void Block_SizeChanged(object? sender, SizeChangedEventArgs e)
|
|
|
+ {
|
|
|
+ LabelCanvas.Width = LabelCanvas.Children.OfType<FrameworkElement>().Select(x => x.ActualWidth).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;
|
|
|
+ block.ColumnIndex = i;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if(columns.Count == 0)
|
|
|
+ {
|
|
|
+ columns.Add(new());
|
|
|
+ }
|
|
|
+ return columns;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void UpdateBlock(Block block)
|
|
|
+ {
|
|
|
+ Render(recalculatePositions: true);
|
|
|
+ }
|
|
|
+}
|