Browse Source

Added CalendarControl drag-drop and made thumb dragging snap to grid

Kenric Nugteren 1 week ago
parent
commit
eb324c7045
2 changed files with 323 additions and 37 deletions
  1. 318 37
      inabox.wpf/Forms/CalendarControl/CalendarControl.cs
  2. 5 0
      inabox.wpf/WPFUtils.cs

+ 318 - 37
inabox.wpf/Forms/CalendarControl/CalendarControl.cs

@@ -17,6 +17,7 @@ using System.Windows;
 using System.Windows.Controls;
 using System.Windows.Controls.Primitives;
 using System.Windows.Data;
+using System.Windows.Documents;
 using System.Windows.Input;
 using System.Windows.Media;
 using System.Windows.Media.Animation;
@@ -462,6 +463,11 @@ public class CalendarControl : ContentControl
         MainScroll.SizeChanged += MainScroll_SizeChanged;
         MainScroll.ScrollChanged += MainScroll_ScrollChanged;
         MainScroll.PreviewMouseWheel += MainScroll_PreviewMouseWheel;
+        MainCanvas.AllowDrop = true;
+
+        MainCanvas.DragEnter += MainCanvas_DragEnter;
+        MainCanvas.DragOver += MainCanvas_DragOver;
+        MainCanvas.Drop += MainCanvas_Drop;
 
         grid.AddChild(
             new Border
@@ -607,6 +613,49 @@ public class CalendarControl : ContentControl
         Render();
     }
 
+    private class BlockDragData(Block block, Point offset)
+    {
+        public Block Block { get; } = block;
+
+        public Point Offset { get; } = offset;
+    }
+
+    private class BlockDragAdorner : Adorner
+    {
+        private Point _offset;
+        public Point Offset
+        {
+            get => _offset;
+            set
+            {
+                _offset = value;
+                InvalidateVisual();
+            }
+        }
+
+        public Size Size { get; set; }
+
+        public BlockDragData Data { get; set; }
+
+        public BlockDragAdorner(UIElement adornedElement, BlockDragData data) : base(adornedElement)
+        {
+            IsHitTestVisible = false;
+            Data = data;
+        }
+
+        protected override void OnMouseMove(MouseEventArgs e)
+        {
+            base.OnMouseMove(e);
+        }
+
+        protected override void OnRender(DrawingContext drawingContext)
+        {
+            var rect = new Rect((Offset - Data.Offset).ToPoint(), Size);
+
+            drawingContext.DrawRoundedRectangle(Colors.Gray.ToBrush(0.5), new Pen(Colors.Black.ToBrush(), 1), rect, 5, 5);
+        }
+    }
+
     private class Block : Border, INotifyPropertyChanged
     {
         public static readonly DependencyProperty ColumnProperty =
@@ -673,25 +722,28 @@ public class CalendarControl : ContentControl
         private Thumb _bottomThumb;
         private TimeSpan _thumbStart;
         private TimeSpan _thumbFinish;
+        private bool _thumbDragging = false;
 
         private CalendarControl _parent;
+        private bool _clicked;
+
+        private BlockDragAdorner? _dragAdorner;
+        private AdornerLayer? _adornerLayer;
 
         public Block(CalendarControl parent, object content)
         {
             _parent = parent;
 
-            var grid = new Grid();
-            var topRow = grid.AddRow(GridUnitType.Auto);
-            grid.AddRow(GridUnitType.Star);
-            grid.AddRow(GridUnitType.Auto);
+            var canvas = new Canvas();
 
             _contentControl = new ContentControl
             {
-                Content = content
+                Content = content,
+                Background = Colors.Transparent.ToBrush()
             };
             _contentControl.Bind(ContentControl.ContentTemplateProperty, parent, x => x.ItemTemplate);
 
-            grid.AddChild(_contentControl, 0, 0, rowSpan: 3);
+            canvas.Children.Add(_contentControl);
 
             _topThumb = new Thumb
             {
@@ -710,32 +762,142 @@ public class CalendarControl : ContentControl
 
             _topThumb.DragStarted += Thumb_DragStarted;
             _topThumb.DragDelta += TopThumb_DragDelta;
-            _topThumb.DragCompleted += Thumb_DragCompleted;
+            _topThumb.DragCompleted += TopThumb_DragCompleted;
 
             _bottomThumb.DragStarted += Thumb_DragStarted;
             _bottomThumb.DragDelta += BottomThumb_DragDelta;
-            _bottomThumb.DragCompleted += Thumb_DragCompleted;
+            _bottomThumb.DragCompleted += BottomThumb_DragCompleted;
+
+            Canvas.SetTop(_topThumb, 0);
+            Canvas.SetTop(_bottomThumb, 0);
+            canvas.Children.Add(_topThumb);
+            canvas.Children.Add(_bottomThumb);
+
+            canvas.SizeChanged += (o, e) =>
+            {
+                _topThumb.Width = canvas.ActualWidth;
+                _bottomThumb.Width = canvas.ActualWidth;
+                _contentControl.Width = canvas.ActualWidth;
+
+                if (!_thumbDragging)
+                {
+                    _contentControl.Height = canvas.ActualHeight;
+
+                    if (_thumbReversed)
+                    {
+                        Canvas.SetTop(_bottomThumb, 0);
+                        Canvas.SetTop(_topThumb, canvas.ActualHeight - _bottomThumb.Height);
+                    }
+                    else
+                    {
+                        Canvas.SetTop(_topThumb, 0);
+                        Canvas.SetTop(_bottomThumb, canvas.ActualHeight - _bottomThumb.Height);
+                    }
+                }
+            };
 
-            grid.AddChild(_topThumb, 0, 0);
-            grid.AddChild(_bottomThumb, 2, 0);
+            MouseMove += Block_MouseMove;
+            GiveFeedback += Block_GiveFeedback;
 
-            Child = grid;
+            ContentControl.MouseLeftButtonDown += (o, e) =>
+            {
+                _clicked = true;
+                if (CanAdjust)
+                {
+                    e.Handled = true;
+                }
+                else
+                {
+                    parent.Block_MouseLeftButtonDown(this, e);
+                }
+            };
+            ContentControl.MouseLeftButtonUp += (o, e) =>
+            {
+                _clicked = false;
+                ClearAdorner();
+                parent.Block_MouseLeftButtonUp(this, e);
+            };
+            ContentControl.MouseRightButtonUp += (o, e) =>
+            {
+                parent.Block_MouseRightButtonUp(this, e);
+            };
+            ContentControl.MouseLeave += ContentControl_MouseLeave;
+
+            Child = canvas;
         }
 
-        private void ReverseThumb()
+        private void Block_GiveFeedback(object sender, GiveFeedbackEventArgs e)
         {
-            _thumbReversed = !_thumbReversed;
-            if (_thumbReversed)
+            if(_dragAdorner is not null)
             {
-                Grid.SetRow(_topThumb, 2);
-                Grid.SetRow(_bottomThumb, 0);
+                var mPos = System.Windows.Forms.Control.MousePosition;
+
+                var thisPos = _parent.MainCanvas.PointFromScreen(PointToScreen(new()));
+                var point = _parent.MainCanvas.PointFromScreen(new(mPos.X, mPos.Y));
+
+                if(_parent.TryGetColumnFromPosition(point, out var _date, out var _column, out var x, out var width))
+                {
+                    var rowIdx = (int)Math.Floor(point.Y / _parent._rowHeight);
+
+                    _dragAdorner.Size = new(width, ActualHeight);
+                    _dragAdorner.Offset = (new Point(x + _dragAdorner.Data.Offset.X, rowIdx * _parent._rowHeight + _dragAdorner.Data.Offset.Y) - thisPos).ToPoint();
+                }
             }
-            else
+        }
+
+        private void ContentControl_MouseLeave(object sender, MouseEventArgs e)
+        {
+            _clicked = false;
+        }
+
+        private void Block_MouseMove(object sender, MouseEventArgs e)
+        {
+            if (!CanAdjust || !_clicked) return;
+
+            if(e.LeftButton == MouseButtonState.Pressed)
             {
-                Grid.SetRow(_topThumb, 0);
-                Grid.SetRow(_bottomThumb, 2);
+                ClearAdorner();
+
+                var data = new BlockDragData(this, e.GetPosition(this));
+                _dragAdorner = new(this, data);
+                _adornerLayer = AdornerLayer.GetAdornerLayer(this);
+                _adornerLayer.Add(_dragAdorner);
+                DragDrop.DoDragDrop(this, data, DragDropEffects.Move);
+                ClearAdorner();
             }
         }
+
+        private void ClearAdorner()
+        {
+            if (_adornerLayer is null || _dragAdorner is null) return;
+
+            _adornerLayer.Remove(_dragAdorner);
+            _dragAdorner = null;
+        }
+
+        private void CancelThumb()
+        {
+            _thumbDragging = false;
+
+            var topY = _parent.GetRow(StartTime) * _parent._rowHeight;
+            var bottomY = _parent.GetRow(EndTime) * _parent._rowHeight;
+
+            var (topThumb, bottomThumb) = _thumbReversed ? (_bottomThumb, _topThumb) : (_topThumb, _bottomThumb);
+
+            Canvas.SetTop(this, topY);
+            Height = bottomY - topY;
+
+            Canvas.SetTop(topThumb, 0);
+            Canvas.SetTop(bottomThumb, Height - bottomThumb.Height);
+
+            Canvas.SetTop(_contentControl, 0);
+            _contentControl.Height = Height;
+        }
+
+        private void ReverseThumb()
+        {
+            _thumbReversed = !_thumbReversed;
+        }
         private void MoveThumb(bool isTop, double amount)
         {
             if (_thumbReversed)
@@ -765,16 +927,62 @@ public class CalendarControl : ContentControl
             var topY = _parent.GetRow(_thumbStart) * _parent._rowHeight;
             var bottomY = _parent.GetRow(_thumbFinish) * _parent._rowHeight;
 
+            var roundedTopY = !isTop ? topY : Math.Floor(_parent.GetRow(_thumbStart)) * _parent._rowHeight;
+            var roundedBottomY = isTop ? bottomY : Math.Ceiling(_parent.GetRow(_thumbFinish)) * _parent._rowHeight;
+
+            var y = Math.Min(topY, roundedTopY);
+            Canvas.SetTop(this, y);
+            Height = Math.Max(bottomY, roundedBottomY) - y;
+
+            var (topThumb, bottomThumb) = _thumbReversed ? (_bottomThumb, _topThumb) : (_topThumb, _bottomThumb);
+
+            Canvas.SetTop(topThumb, topY - y);
+            Canvas.SetTop(bottomThumb, bottomY - y - bottomThumb.Height);
+
+            Canvas.SetTop(_contentControl, roundedTopY - y);
+            _contentControl.Height = roundedBottomY - roundedTopY;
+
+            Logger.Send(LogType.Information, "", $"{Height} - {_contentControl.Height}");
+        }
+        private void DragCompleted(bool isTop)
+        {
+            if (_thumbReversed)
+            {
+                isTop = !isTop;
+            }
+
+            if (isTop)
+            {
+                _thumbStart = Math.Floor(_parent.GetRow(_thumbStart)) * _parent.RowInterval + _parent.StartHour;
+            }
+            else
+            {
+                _thumbFinish = Math.Ceiling(_parent.GetRow(_thumbFinish)) * _parent.RowInterval + _parent.StartHour;
+            }
+            _thumbDragging = false;
+            StartTime = _thumbStart;
+            EndTime = _thumbFinish;
+
+            var topY = _parent.GetRow(_thumbStart) * _parent._rowHeight;
+            var bottomY = _parent.GetRow(_thumbFinish) * _parent._rowHeight;
+
+            var (topThumb, bottomThumb) = _thumbReversed ? (_bottomThumb, _topThumb) : (_topThumb, _bottomThumb);
+
             Canvas.SetTop(this, topY);
             Height = bottomY - topY;
+
+            Canvas.SetTop(topThumb, 0);
+            Canvas.SetTop(bottomThumb, Height - bottomThumb.Height);
+
+            Canvas.SetTop(_contentControl, 0);
+            _contentControl.Height = Height;
         }
 
         private void Thumb_DragStarted(object sender, DragStartedEventArgs e)
         {
             _thumbStart = StartTime;
             _thumbFinish = EndTime;
-
-            _thumbReversed = false;
+            _thumbDragging = true;
         }
 
         private void TopThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
@@ -782,10 +990,13 @@ public class CalendarControl : ContentControl
             MoveThumb(true, e.VerticalChange);
         }
 
-        private void Thumb_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
+        private void TopThumb_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
         {
-            StartTime = _thumbStart;
-            EndTime = _thumbFinish;
+            DragCompleted(true);
+        }
+        private void BottomThumb_DragCompleted(object sender, System.Windows.Controls.Primitives.DragCompletedEventArgs e)
+        {
+            DragCompleted(false);
         }
 
         private void BottomThumb_DragDelta(object sender, System.Windows.Controls.Primitives.DragDeltaEventArgs e)
@@ -875,19 +1086,6 @@ public class CalendarControl : ContentControl
             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);
-            };
-            block.ContentControl.MouseRightButtonUp += (o, e) =>
-            {
-                Block_MouseRightButtonUp(block, e);
-            };
-
             _blockList.Add(block);
         }
         return true;
@@ -1330,6 +1528,43 @@ public class CalendarControl : ContentControl
         }
     }
 
+    private bool TryGetColumnFromPosition(Point point, out DateTime date, [NotNullWhen(true)] out object? column, out double columnX, out double width)
+    {
+        column = null;
+        date = DateTime.MinValue;
+        columnX = 0.0;
+        width = 0.0;
+
+        var x = point.X;
+        foreach(var (columnDate, columnKey) in _columnList)
+        {
+            if (!_blocks.TryGetValue(columnDate, out var dateBlocks)
+                || !dateBlocks.TryGetValue(columnKey, out var columnBlocks)) continue;
+
+            var (nCols, subColWidth) = ColumnWidthMode switch
+            {
+                CalendarColumnWidthMode.ConstantSubColumns => (columnBlocks.Columns!.Count, _colWidth),
+                CalendarColumnWidthMode.ConstantColumns => (1, _colWidth / columnBlocks.Columns!.Count),
+                _ => throw new InvalidEnumException<CalendarColumnWidthMode>(ColumnWidthMode)
+            };
+            var colWidth = nCols * _colWidth + _colSpace;
+
+            if(x < colWidth)
+            {
+                column = columnKey;
+                date = columnDate;
+                width = colWidth;
+                break;
+            }
+            else
+            {
+                columnX += colWidth;
+                x -= colWidth;
+            }
+        }
+        return column is not null;
+    }
+
     private bool TryGetBlockFromPosition(Point point, out DateTime blockDate, [NotNullWhen(true)] out object? column, out TimeSpan start, out TimeSpan end, out int index)
     {
         var rowIdx = (int)Math.Floor(point.Y / _rowHeight);
@@ -1419,6 +1654,52 @@ public class CalendarControl : ContentControl
         CallBlockEvent(e, block, args.GetPosition(MainCanvas));
     }
 
+    private void MainCanvas_DragOver(object sender, DragEventArgs e)
+    {
+        e.Effects = DragDropEffects.None;
+
+        if (e.Data.GetDataPresent(typeof(BlockDragData)))
+        {
+            e.Effects = DragDropEffects.Move;
+            // var data = e.Data.GetData(typeof(Block));
+        }
+    }
+
+    private void MainCanvas_Drop(object sender, DragEventArgs e)
+    {
+        if (e.Data.GetDataPresent(typeof(BlockDragData)))
+        {
+            var pos = e.GetPosition(MainCanvas);
+            if (e.Data.GetData(typeof(BlockDragData)) is not BlockDragData data) return;
+
+            if (TryGetBlockFromPosition(pos, out var date, out var column, out var start, out var end, out var index))
+            {
+                var time = StartHour + RowInterval * Math.Floor(pos.Y / _rowHeight);
+                var duration = data.Block.EndTime - data.Block.StartTime;
+                var endTime = time + duration;
+                var min = TimeSpan.Zero.Add(TimeSpan.FromTicks(1)); // This is really, really stupid, but needs to be so that the assignment doesn't fall back to booked time.
+                var max = TimeSpan.FromHours(24);
+                if(time < min)
+                {
+                    time = min;
+                }
+                if(endTime > max)
+                {
+                    endTime = max;
+                }
+
+                data.Block.Column = column;
+                data.Block.Date = date;
+                data.Block.StartTime = time;
+                data.Block.EndTime = endTime;
+            }
+        }
+    }
+
+    private void MainCanvas_DragEnter(object sender, DragEventArgs e)
+    {
+    }
+
     private void Block_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
     {
         if (sender is not Block block) return;

+ 5 - 0
inabox.wpf/WPFUtils.cs

@@ -703,4 +703,9 @@ public static class WPFUtils
         element.Name = name;
         return element;
     }
+
+    public static System.Windows.Point ToPoint(this Vector vector)
+    {
+        return new(vector.X, vector.Y);
+    }
 }