using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Shapes; using Azaion.Annotator.DTO; using MediatR; using Color = System.Windows.Media.Color; using Rectangle = System.Windows.Shapes.Rectangle; namespace Azaion.Annotator.Controls; public class CanvasEditor : Canvas { private Point _lastPos; public SelectionState SelectionState { get; set; } = SelectionState.None; private readonly Rectangle _newAnnotationRect; private Point _newAnnotationStartPos; private readonly Line _horizontalLine; private readonly Line _verticalLine; private readonly TextBlock _classNameHint; private Rectangle _curRec = new(); private AnnotationControl _curAnn = null!; private const int MIN_SIZE = 20; private readonly TimeSpan _viewThreshold = TimeSpan.FromMilliseconds(400); public FormState FormState { get; set; } = null!; public IMediator Mediator { get; set; } = null!; public static readonly DependencyProperty GetTimeFuncProp = DependencyProperty.Register( nameof(GetTimeFunc), typeof(Func), typeof(CanvasEditor), new PropertyMetadata(null)); public Func GetTimeFunc { get => (Func)GetValue(GetTimeFuncProp); set => SetValue(GetTimeFuncProp, value); } private AnnotationClass _currentAnnClass = null!; public AnnotationClass CurrentAnnClass { get => _currentAnnClass; set { _verticalLine.Stroke = value.ColorBrush; _verticalLine.Fill = value.ColorBrush; _horizontalLine.Stroke = value.ColorBrush; _horizontalLine.Fill = value.ColorBrush; _classNameHint.Text = value.Name; _newAnnotationRect.Stroke = value.ColorBrush; _newAnnotationRect.Fill = value.ColorBrush; _currentAnnClass = value; } } public readonly List CurrentAnns = new(); public CanvasEditor() { _horizontalLine = new Line { HorizontalAlignment = HorizontalAlignment.Stretch, Stroke = new SolidColorBrush(Colors.Blue), Fill = new SolidColorBrush(Colors.Blue), StrokeDashArray = [5], StrokeThickness = 2 }; _verticalLine = new Line { VerticalAlignment = VerticalAlignment.Stretch, Stroke = new SolidColorBrush(Colors.Blue), Fill = new SolidColorBrush(Colors.Blue), StrokeDashArray = [5], StrokeThickness = 2 }; _classNameHint = new TextBlock { Text = CurrentAnnClass?.Name ?? "asd", Foreground = new SolidColorBrush(Colors.Black), Cursor = Cursors.Arrow, FontSize = 16, FontWeight = FontWeights.Bold }; _newAnnotationRect = new Rectangle { Name = "selector", Height = 0, Width = 0, Stroke = new SolidColorBrush(Colors.Gray), Fill = new SolidColorBrush(Color.FromArgb(128, 128, 128, 128)), }; KeyDown += (_, args) => { Console.WriteLine($"pressed {args.Key}"); }; MouseDown += CanvasMouseDown; MouseMove += CanvasMouseMove; MouseUp += CanvasMouseUp; SizeChanged += CanvasResized; Cursor = Cursors.Cross; Children.Add(_newAnnotationRect); Children.Add(_horizontalLine); Children.Add(_verticalLine); Children.Add(_classNameHint); Loaded += Init; } private void Init(object sender, RoutedEventArgs e) { _horizontalLine.X1 = 0; _horizontalLine.X2 = ActualWidth; _verticalLine.Y1 = 0; _verticalLine.Y2 = ActualHeight; } private void CanvasMouseDown(object sender, MouseButtonEventArgs e) { ClearSelections(); NewAnnotationStart(sender, e); } private void CanvasMouseMove(object sender, MouseEventArgs e) { var pos = e.GetPosition(this); _horizontalLine.Y1 = _horizontalLine.Y2 = pos.Y; _verticalLine.X1 = _verticalLine.X2 = pos.X; SetLeft(_classNameHint, pos.X + 10); SetTop(_classNameHint, pos.Y - 30); if (e.LeftButton != MouseButtonState.Pressed) return; if (SelectionState == SelectionState.NewAnnCreating) NewAnnotationCreatingMove(sender, e); if (SelectionState == SelectionState.AnnResizing) AnnotationResizeMove(sender, e); if (SelectionState == SelectionState.AnnMoving) AnnotationPositionMove(sender, e); } private void CanvasMouseUp(object sender, MouseButtonEventArgs e) { if (SelectionState == SelectionState.NewAnnCreating) CreateAnnotation(e.GetPosition(this)); SelectionState = SelectionState.None; e.Handled = true; } private void CanvasResized(object sender, SizeChangedEventArgs e) { _horizontalLine.X2 = e.NewSize.Width; _verticalLine.Y2 = e.NewSize.Height; } #region Annotation Resizing & Moving private void AnnotationResizeStart(object sender, MouseEventArgs e) { SelectionState = SelectionState.AnnResizing; _lastPos = e.GetPosition(this); _curRec = (Rectangle)sender; _curAnn = (AnnotationControl)((Grid)_curRec.Parent).Parent; e.Handled = true; } private void AnnotationResizeMove(object sender, MouseEventArgs e) { if (SelectionState != SelectionState.AnnResizing) return; var currentPos = e.GetPosition(this); var x = GetLeft(_curAnn); var y = GetTop(_curAnn); var offsetX = currentPos.X - _lastPos.X; var offsetY = currentPos.Y - _lastPos.Y; switch (_curRec.HorizontalAlignment, _curRec.VerticalAlignment) { case (HorizontalAlignment.Left, VerticalAlignment.Top): _curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX); _curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY); SetLeft(_curAnn, x + offsetX); SetTop(_curAnn, y + offsetY); break; case (HorizontalAlignment.Center, VerticalAlignment.Top): _curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY); SetTop(_curAnn, y + offsetY); break; case (HorizontalAlignment.Right, VerticalAlignment.Top): _curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX); _curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height - offsetY); SetTop(_curAnn, y + offsetY); break; case (HorizontalAlignment.Left, VerticalAlignment.Center): _curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX); SetLeft(_curAnn, x + offsetX); break; case (HorizontalAlignment.Right, VerticalAlignment.Center): _curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX); break; case (HorizontalAlignment.Left, VerticalAlignment.Bottom): _curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width - offsetX); _curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY); SetLeft(_curAnn, x + offsetX); break; case (HorizontalAlignment.Center, VerticalAlignment.Bottom): _curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY); break; case (HorizontalAlignment.Right, VerticalAlignment.Bottom): _curAnn.Width = Math.Max(MIN_SIZE, _curAnn.Width + offsetX); _curAnn.Height = Math.Max(MIN_SIZE, _curAnn.Height + offsetY); break; } _lastPos = currentPos; } private void AnnotationPositionStart(object sender, MouseEventArgs e) { _lastPos = e.GetPosition(this); _curAnn = (AnnotationControl)sender; if (!Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl)) ClearSelections(); _curAnn.IsSelected = true; SelectionState = SelectionState.AnnMoving; e.Handled = true; } private void AnnotationPositionMove(object sender, MouseEventArgs e) { if (SelectionState != SelectionState.AnnMoving) return; var currentPos = e.GetPosition(this); var offsetX = currentPos.X - _lastPos.X; var offsetY = currentPos.Y - _lastPos.Y; SetLeft(_curAnn, GetLeft(_curAnn) + offsetX); SetTop(_curAnn, GetTop(_curAnn) + offsetY); _lastPos = currentPos; e.Handled = true; } #endregion #region NewAnnotation private void NewAnnotationStart(object sender, MouseButtonEventArgs e) { _newAnnotationStartPos = e.GetPosition(this); SetLeft(_newAnnotationRect, _newAnnotationStartPos.X); SetTop(_newAnnotationRect, _newAnnotationStartPos.Y); _newAnnotationRect.MouseMove += NewAnnotationCreatingMove; SelectionState = SelectionState.NewAnnCreating; } private void NewAnnotationCreatingMove(object sender, MouseEventArgs e) { if (SelectionState != SelectionState.NewAnnCreating) return; var currentPos = e.GetPosition(this); var diff = currentPos - _newAnnotationStartPos; _newAnnotationRect.Height = Math.Abs(diff.Y); _newAnnotationRect.Width = Math.Abs(diff.X); if (diff.X < 0) SetLeft(_newAnnotationRect, currentPos.X); if (diff.Y < 0) SetTop(_newAnnotationRect, currentPos.Y); } private void CreateAnnotation(Point endPos) { _newAnnotationRect.Width = 0; _newAnnotationRect.Height = 0; var width = Math.Abs(endPos.X - _newAnnotationStartPos.X); var height = Math.Abs(endPos.Y - _newAnnotationStartPos.Y); if (width < MIN_SIZE || height < MIN_SIZE) return; var time = GetTimeFunc(); CreateAnnotation(CurrentAnnClass, time, new CanvasLabel { Width = width, Height = height, X = Math.Min(endPos.X, _newAnnotationStartPos.X), Y = Math.Min(endPos.Y, _newAnnotationStartPos.Y) }); } public AnnotationControl CreateAnnotation(AnnotationClass annClass, TimeSpan? time, CanvasLabel canvasLabel) { var annotationControl = new AnnotationControl(annClass, time, AnnotationResizeStart, canvasLabel.Probability) { Width = canvasLabel.Width, Height = canvasLabel.Height }; annotationControl.MouseDown += AnnotationPositionStart; SetLeft(annotationControl, canvasLabel.X ); SetTop(annotationControl, canvasLabel.Y); Children.Add(annotationControl); CurrentAnns.Add(annotationControl); _newAnnotationRect.Fill = new SolidColorBrush(annClass.Color); return annotationControl; } #endregion private void RemoveAnnotations(IEnumerable listToRemove) { foreach (var ann in listToRemove) { Children.Remove(ann); CurrentAnns.Remove(ann); } } public void RemoveAllAnns() { foreach (var ann in CurrentAnns) Children.Remove(ann); CurrentAnns.Clear(); } public void RemoveSelectedAnns() => RemoveAnnotations(CurrentAnns.Where(x => x.IsSelected).ToList()); private void ClearSelections() { foreach (var ann in CurrentAnns) ann.IsSelected = false; } public void ClearExpiredAnnotations(TimeSpan time) { var expiredAnns = CurrentAnns.Where(x => x.Time.HasValue && Math.Abs((time - x.Time.Value).TotalMilliseconds) > _viewThreshold.TotalMilliseconds) .ToList(); RemoveAnnotations(expiredAnns); } }