Sfoglia il codice sorgente

avalonia: Began implementation of CalendarView

Kenric Nugteren 1 mese fa
parent
commit
2e9ead53c8

+ 31 - 0
InABox.Avalonia/Components/CalendarView/CalendarView.axaml

@@ -0,0 +1,31 @@
+<UserControl xmlns="https://github.com/avaloniaui"
+             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
+             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
+             mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
+             x:Class="InABox.Avalonia.Components.CalendarView">
+    <Grid ColumnDefinitions="Auto,*"
+          RowDefinitions="Auto,*">
+        <Border Grid.Column="0" Grid.Row="0"
+                BorderBrush="LightGray" BorderThickness="0,0,1,0">
+            
+        </Border>
+        <Border Grid.Column="0" Grid.Row="1"
+                BorderBrush="LightGray" BorderThickness="0,0,1,0">
+            <ScrollViewer Name="LabelScroll" VerticalScrollBarVisibility="Hidden">
+                <Canvas Name="LabelCanvas" Margin="2,0"/>
+            </ScrollViewer>
+        </Border>
+        <Border Grid.Column="1" Grid.Row="0"
+                BorderBrush="LightGray" BorderThickness="0,0,0,1">
+            <ScrollViewer Name="HeaderScroll" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Disabled">
+                <Canvas Name="HeaderCanvas"/>
+            </ScrollViewer>
+        </Border>
+        <ScrollViewer Name="ScrollViewer"
+                      Grid.Row="1" Grid.Column="1"
+                      HorizontalScrollBarVisibility="Auto" SizeChanged="Scroll_SizeChanged">
+            <Canvas Name="Canvas"/>
+        </ScrollViewer>
+    </Grid>
+</UserControl>

+ 444 - 0
InABox.Avalonia/Components/CalendarView/CalendarView.axaml.cs

@@ -0,0 +1,444 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Shapes;
+using Avalonia.Controls.Templates;
+using Avalonia.Data;
+using Avalonia.Media;
+using Avalonia.Metadata;
+using DynamicData.Binding;
+using InABox.Core;
+using System.Collections;
+using System.Collections.Specialized;
+using System.Reactive.Linq;
+using System.Runtime.Serialization;
+
+namespace InABox.Avalonia.Components;
+
+public partial class CalendarView : UserControl
+{
+    public static readonly StyledProperty<double> RowHeightProperty = 
+        AvaloniaProperty.Register<CalendarView, double>(nameof(RowHeight), defaultValue: 100);
+
+    public static readonly StyledProperty<double> MinimumColumnWidthProperty = 
+        AvaloniaProperty.Register<CalendarView, double>(nameof(MinimumColumnWidth), defaultValue: 50);
+
+    public static readonly StyledProperty<TimeSpan> RowIntervalProperty = 
+        AvaloniaProperty.Register<CalendarView, TimeSpan>(nameof(RowInterval), defaultValue: TimeSpan.FromHours(1));
+
+    public static readonly StyledProperty<IBinding?> ColumnMappingProperty = 
+        AvaloniaProperty.Register<CalendarView, IBinding?>(nameof(ColumnMapping));
+
+    public static readonly StyledProperty<IBinding?> StartTimeMappingProperty = 
+        AvaloniaProperty.Register<CalendarView, IBinding?>(nameof(StartTimeMapping));
+
+    public static readonly StyledProperty<IBinding?> EndTimeMappingProperty = 
+        AvaloniaProperty.Register<CalendarView, IBinding?>(nameof(EndTimeMapping));
+
+    public static readonly StyledProperty<IEnumerable?> ItemsSourceProperty = 
+        AvaloniaProperty.Register<CalendarView, IEnumerable?>(nameof(ItemsSource));
+
+    public static readonly StyledProperty<IDataTemplate?> ItemTemplateProperty = 
+        AvaloniaProperty.Register<CalendarView, IDataTemplate?>(nameof(ItemTemplate));
+
+    public static readonly StyledProperty<IDataTemplate?> HeaderTemplateProperty = 
+        AvaloniaProperty.Register<CalendarView, IDataTemplate?>(nameof(HeaderTemplate));
+
+    public double MinimumColumnWidth
+    {
+        get => GetValue(MinimumColumnWidthProperty);
+        set => SetValue(MinimumColumnWidthProperty, value);
+    }
+
+    public double RowHeight
+    {
+        get => GetValue(RowHeightProperty);
+        set => SetValue(RowHeightProperty, value);
+    }
+
+    public TimeSpan RowInterval
+    {
+        get => GetValue(RowIntervalProperty);
+        set => SetValue(RowIntervalProperty, value);
+    }
+
+    [AssignBinding]
+    [InheritDataTypeFromItems(nameof(ItemsSource))]
+    public IBinding? ColumnMapping
+    {
+        get => GetValue(ColumnMappingProperty);
+        set => SetValue(ColumnMappingProperty, value);
+    }
+
+    [AssignBinding]
+    [InheritDataTypeFromItems(nameof(ItemsSource))]
+    public IBinding? StartTimeMapping
+    {
+        get => GetValue(StartTimeMappingProperty);
+        set => SetValue(StartTimeMappingProperty, value);
+    }
+
+    [AssignBinding]
+    [InheritDataTypeFromItems(nameof(ItemsSource))]
+    public IBinding? EndTimeMapping
+    {
+        get => GetValue(EndTimeMappingProperty);
+        set => SetValue(EndTimeMappingProperty, value);
+    }
+
+    public IEnumerable? ItemsSource
+    {
+        get => GetValue(ItemsSourceProperty);
+        set => SetValue(ItemsSourceProperty, value);
+    }
+
+    [InheritDataTypeFromItems(nameof(ItemsSource))]
+    public IDataTemplate? ItemTemplate
+    {
+        get => GetValue(ItemTemplateProperty);
+        set => SetValue(ItemTemplateProperty, value);
+    }
+
+    public IDataTemplate? HeaderTemplate
+    {
+        get => GetValue(HeaderTemplateProperty);
+        set => SetValue(HeaderTemplateProperty, value);
+    }
+
+    static CalendarView()
+    {
+        ItemsSourceProperty.Changed.AddClassHandler<CalendarView>(ItemsSource_Changed);
+        RowHeightProperty.Changed.AddClassHandler<CalendarView>(Render_Changed);
+        MinimumColumnWidthProperty.Changed.AddClassHandler<CalendarView>(Render_Changed);
+        RowIntervalProperty.Changed.AddClassHandler<CalendarView>(Render_Changed);
+    }
+
+    private static void Render_Changed(CalendarView view, AvaloniaPropertyChangedEventArgs args)
+    {
+        view.Render();
+    }
+
+    public CalendarView()
+    {
+        InitializeComponent();
+
+        ScrollViewer.GetPropertyChangedObservable(ScrollViewer.OffsetProperty).Subscribe(x =>
+        {
+            LabelScroll.Offset = new(0, ScrollViewer.Offset.Y);
+            HeaderScroll.Offset = new(ScrollViewer.Offset.X, 0);
+        });
+    }
+
+    private static void ItemsSource_Changed(CalendarView view, AvaloniaPropertyChangedEventArgs args)
+    {
+        if(args.OldValue is INotifyCollectionChanged oldNotify)
+        {
+            oldNotify.CollectionChanged -= view.Collection_Changed;
+        }
+        view.Render(itemsChanged: true);
+        if(view.ItemsSource is INotifyCollectionChanged notify)
+        {
+            notify.CollectionChanged += view.Collection_Changed;
+        }
+    }
+
+    private void Collection_Changed(object? sender, NotifyCollectionChangedEventArgs e)
+    {
+        Render(itemsChanged: true);
+    }
+
+    private void Scroll_SizeChanged(object? sender, SizeChangedEventArgs e)
+    {
+        Render();
+    }
+
+    private class Block : ContentControl
+    {
+        public static readonly StyledProperty<object?> ColumnProperty =
+            AvaloniaProperty.Register<Block, object?>(nameof(Column));
+        public static readonly StyledProperty<TimeSpan> StartTimeProperty =
+            AvaloniaProperty.Register<Block, TimeSpan>(nameof(StartTime));
+        public static readonly StyledProperty<TimeSpan> EndTimeProperty =
+            AvaloniaProperty.Register<Block, TimeSpan>(nameof(EndTime));
+
+        public int NColumns { get; set; } = -1;
+
+        public object? Column
+        {
+            get => GetValue(ColumnProperty);
+            set => SetValue(ColumnProperty, value);
+        }
+
+        public TimeSpan StartTime
+        {
+            get => GetValue(StartTimeProperty);
+            set => SetValue(StartTimeProperty, value);
+        }
+
+        public TimeSpan EndTime
+        {
+            get => GetValue(EndTimeProperty);
+            set => SetValue(EndTimeProperty, value);
+        }
+
+        public override string ToString()
+        {
+            return $"Block({Column:dd/MM/yyyy}: {StartTime:hh\\:mm} - {EndTime:hh\\:mm})";
+        }
+    }
+
+    private class Column
+    {
+        public List<Block> Blocks { get; set; } = new();
+
+        public List<List<Block>>? Columns { get; set; } = null;
+    }
+
+    private Dictionary<object, Column> _blocks = new();
+
+    private List<IDisposable> _oldSubscriptions = new();
+
+    private void RecreateBlocksList()
+    {
+        if (ItemsSource is null) return;
+
+        var dateBinding = ColumnMapping;
+        var startBinding = StartTimeMapping;
+        var endBinding = EndTimeMapping;
+        if(dateBinding is null || startBinding is null || endBinding is null)
+        {
+            return;
+        }
+
+        foreach(var subscription in _oldSubscriptions)
+        {
+            subscription.Dispose();
+        }
+        _oldSubscriptions.Clear();
+        _blocks.Clear();
+
+        foreach(var item in ItemsSource)
+        {
+            var block = new Block
+            {
+                [!Block.ColumnProperty] = dateBinding,
+                [!Block.StartTimeProperty] = startBinding,
+                [!Block.EndTimeProperty] = endBinding,
+                [Block.DataContextProperty] = item,
+                [!Block.ContentTemplateProperty] = this[!ItemTemplateProperty],
+                Content = item
+            };
+            _oldSubscriptions.Add(block.GetObservable(Block.ColumnProperty).Skip(1).Subscribe(x => Render(itemsChanged: true)));
+            _oldSubscriptions.Add(block.GetObservable(Block.StartTimeProperty).Skip(1).Subscribe(x => UpdateBlock(block)));
+            _oldSubscriptions.Add(block.GetObservable(Block.EndTimeProperty).Skip(1).Subscribe(x => UpdateBlock(block)));
+
+            var column = block.Column;
+            if(column is null)
+            {
+                continue;
+            }
+            _blocks.GetValueOrAdd(column).Blocks.Add(block);
+        }
+    }
+
+    private void Render(bool itemsChanged = false, bool recalculatePositions = false)
+    {
+        if (itemsChanged)
+        {
+            RecreateBlocksList();
+        }
+        if (recalculatePositions)
+        {
+            foreach (var (column, columnBlocks) in _blocks)
+            {
+                columnBlocks.Columns = null;
+            }
+        }
+
+        var nRows = (24 / RowInterval.TotalHours);
+        var rowHeight = Math.Max(RowHeight, ScrollViewer.Bounds.Height / nRows);
+
+        Canvas.Children.Clear();
+        Canvas.Height = rowHeight * nRows;
+
+        var minColWidth = 50;
+        var colSpace = 1;
+
+        var nColumns = 0;
+        foreach (var (column, columnBlocks) in _blocks)
+        {
+            columnBlocks.Columns ??= RecalculateBlockPositionsForDay(columnBlocks.Blocks);
+            // columnsPerDay = Math.Max(columnsPerDay, columnBlocks.Columns.Count);
+            nColumns += columnBlocks.Columns.Count;
+        }
+        // nColumns = columnsPerDay * _blocks.Count
+
+        var colWidth = (Math.Max((ScrollViewer.Bounds.Width - colSpace * (_blocks.Count - 1)) / nColumns, minColWidth));
+
+        HeaderCanvas.Children.Clear();
+
+        var colX = 0.0;
+        var i = 0;
+        foreach(var (columnKey, columnBlocks) in _blocks)
+        {
+            var contentControl = new ContentControl
+            {
+                Content = columnKey,
+                Width = colWidth * columnBlocks.Columns!.Count,
+                [!Block.ContentTemplateProperty] = this[!HeaderTemplateProperty],
+            };
+            Canvas.SetLeft(contentControl, colX);
+            HeaderCanvas.Children.Add(contentControl);
+            contentControl.SizeChanged += ContentControl_SizeChanged;
+
+            foreach(var column in columnBlocks.Columns!)
+            {
+                foreach(var block in column)
+                {
+                    Canvas.SetTop(block, GetRow(block.StartTime) * rowHeight);
+                    Canvas.SetLeft(block, colX);
+                    block.Height = Math.Max((GetRow(block.EndTime) - GetRow(block.StartTime)) * rowHeight, 5);
+                    block.Width = colWidth * block.NColumns;
+                    Canvas.Children.Add(block);
+                }
+
+                colX += colWidth;
+            }
+
+            if(i < _blocks.Count - 1)
+            {
+                var rectangle = new Rectangle
+                {
+                    Width = 0.75,
+                    Height = Canvas.Height,
+                    Fill = new SolidColorBrush(Colors.LightGray)
+                };
+                Canvas.SetLeft(rectangle, colX);
+                Canvas.Children.Add(rectangle);
+
+                var headRectangle = new Rectangle
+                {
+                    Width = 0.75,
+                    [!Rectangle.HeightProperty] = HeaderCanvas.WhenValueChanged(x => x.Bounds)
+                        .Select(x => x.Height)
+                        .ToBinding(),
+                    Fill = new SolidColorBrush(Colors.LightGray)
+                };
+                Canvas.SetLeft(headRectangle, colX);
+                HeaderCanvas.Children.Add(headRectangle);
+
+                colX += colSpace;
+            }
+            ++i;
+        }
+
+        Canvas.Width = colX;
+        HeaderCanvas.Width = colX;
+
+        var lines = new List<Control>();
+
+        LabelCanvas.Children.Clear();
+        LabelCanvas.Height = Canvas.Height;
+
+        var y = rowHeight;
+        for(var time = RowInterval; time < TimeSpan.FromHours(24); time += RowInterval)
+        {
+            var rectangle = new Rectangle
+            {
+                Width = Canvas.Width,
+                Height = 0.75,
+                Fill = new SolidColorBrush(Colors.LightGray)
+            };
+            Canvas.SetLeft(rectangle, 0);
+            Canvas.SetTop(rectangle, y);
+            lines.Add(rectangle);
+
+            var block = new TextBlock
+            {
+                Text = time.ToString("hh\\:mm"),
+                Margin = new(0, -5, 0, 0)
+            }.WithClass("ExtraSmall");
+            block.SizeChanged += Block_SizeChanged;
+            Canvas.SetTop(block, y);
+            LabelCanvas.Children.Add(block);
+
+            y += rowHeight;
+        }
+
+        Canvas.Children.InsertRange(0, lines);
+    }
+
+    private void ContentControl_SizeChanged(object? sender, SizeChangedEventArgs e)
+    {
+        HeaderCanvas.Height = HeaderCanvas.Children.Select(x => x.Bounds.Height).Max();
+    }
+
+    private void Block_SizeChanged(object? sender, SizeChangedEventArgs e)
+    {
+        LabelCanvas.Width = LabelCanvas.Children.Select(x => x.Bounds.Width).Max();
+    }
+
+    private double GetRow(TimeSpan time)
+    {
+        return time.TotalHours / RowInterval.TotalHours;
+    }
+
+    private static List<List<Block>> RecalculateBlockPositionsForDay(List<Block> dayBlocks)
+    {
+        dayBlocks.SortBy(x => x.StartTime);
+        var columns = new List<List<Block>>();
+
+        var remainingBlocks = dayBlocks;
+        while(remainingBlocks.Count > 0)
+        {
+            // At least one block will be moved, so we can use 1 less than the remaining as capacity.
+            var tempRemainingBlocks = new List<Block>(remainingBlocks.Count - 1);
+            var newBlocks = new List<Block>(remainingBlocks.Count);
+            var curTime = TimeSpan.MinValue;
+            Block? curBlock = null;
+            foreach(var block in remainingBlocks)
+            {
+                if(curBlock is not null && block.StartTime < curTime)
+                {
+                    tempRemainingBlocks.Add(block);
+                }
+                else
+                {
+                    newBlocks.Add(block);
+                    curTime = block.EndTime;
+                    curBlock = block;
+                }
+            }
+            columns.Add(newBlocks);
+            remainingBlocks = tempRemainingBlocks;
+        }
+        for(int i = 0; i < columns.Count; ++i)
+        {
+            foreach(var block in columns[i])
+            {
+                var nColumns = -1;
+                for(int j = i + 1; j < columns.Count; ++j)
+                {
+                    foreach(var block2 in columns[j])
+                    {
+                        if(block.StartTime < block2.EndTime && block.EndTime > block2.StartTime)
+                        {
+                            nColumns = j - i;
+                            break;
+                        }
+                    }
+                    if(nColumns > -1)
+                    {
+                        break;
+                    }
+                }
+                block.NColumns = nColumns > -1 ? nColumns : columns.Count - i;
+            }
+        }
+        return columns;
+    }
+
+    private void UpdateBlock(Block block)
+    {
+        Render(recalculatePositions: true);
+    }
+}

+ 3 - 0
InABox.Avalonia/InABox.Avalonia.csproj

@@ -69,6 +69,9 @@
     </ItemGroup>
 
     <ItemGroup>
+      <Compile Update="Components\CalendarView\CalendarView.axaml.cs">
+        <DependentUpon>CalendarView.axaml</DependentUpon>
+      </Compile>
       <Compile Update="Components\TimeSelector\TimeSelectorView.axaml.cs">
         <DependentUpon>TimeSelectorView.axaml</DependentUpon>
       </Compile>