using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; using OpenCvSharp; using VisionBuilder.UI.Avalonia.Uno.Services; using VisionBuilder.UI.Common; using VisionBuilder.UI.Common.ViewModel; using VisionBuilder.UI.Common.ViewModel.Classes; namespace VisionBuilder.UI.Avalonia.Uno.Views; public partial class SingleCameraView : UserControl { private SingleCameraVM? _vm; private ObservableCollection? _subscribedErrors; private readonly Dictionary _statsFields = new(); private readonly Dictionary _dynamicButtonControls = new(); private const string StartedAtLabel = "Started at"; private const string ProcessingTimeLabel = "Processing time"; private const string BadLabel = "Bad"; private const string TotalLabel = "Total"; private const string ErrorRateLabel = "Error rate"; private const string DetailsLabel = "Details"; public SingleCameraView() { InitializeComponent(); } public void SetViewModel(SingleCameraVM singleCameraVm) { _vm = singleCameraVm; _vm.SynchronizationContext = SynchronizationContext.Current; DataContext = _vm; // Bind commands BtnSelectRecipe.Click += async (_, _) => await _vm.SelectRecipe(); BtnStart.Click += (_, _) => _vm.StartCommand.Execute(null); BtnStop.Click += (_, _) => _vm.StopCommand.Execute(null); BtnPause.Click += (_, _) => _vm.PauseCommand.Execute(null); BtnResume.Click += (_, _) => _vm.ResumeCommand.Execute(null); BtnHotReload.Click += (_, _) => _vm.HotReloadCommand.Execute(null); // Subscribe to VM property changes _vm.PropertyChanged += Vm_PropertyChanged; _vm.PreviewVm.PropertyChanged += PreviewVm_PropertyChanged; _vm.StatisticsVm.PropertyChanged += StatisticsVm_PropertyChanged; // Subscribe to errors collection _subscribedErrors = _vm.ErrorsVm.Errors; _subscribedErrors.CollectionChanged += Errors_CollectionChanged; // Initial UI state UpdateButtonVisibility(); UpdateCameraLabel(); InitializeStatsFields(); // Subscribe to dynamic buttons _vm.DynamicButtons.CollectionChanged += DynamicButtons_CollectionChanged; foreach (var btn in _vm.DynamicButtons) AddDynamicButton(btn); } private void Vm_PropertyChanged(object? sender, PropertyChangedEventArgs e) { Dispatcher.UIThread.Post(() => { switch (e.PropertyName) { case nameof(SingleCameraVM.CameraLabelAndStatus): UpdateCameraLabel(); break; case nameof(SingleCameraVM.CurrentRecipeName): BtnSelectRecipe.Content = _vm!.CurrentRecipeName.ToUpperInvariant(); break; case nameof(SingleCameraVM.CanStart): case nameof(SingleCameraVM.CanStop): case nameof(SingleCameraVM.CanPause): case nameof(SingleCameraVM.CanResume): case nameof(SingleCameraVM.CanHotReload): case nameof(SingleCameraVM.RecipeNotSelected): UpdateButtonVisibility(); break; } }); } private void UpdateCameraLabel() { LblCameraName.Text = _vm?.CameraLabelAndStatus ?? "Camera"; } private void UpdateButtonVisibility() { if (_vm == null) return; BtnStart.IsVisible = _vm.CanStart; BtnStop.IsVisible = _vm.CanStop; BtnPause.IsVisible = _vm.CanPause; BtnResume.IsVisible = _vm.CanResume; BtnHotReload.IsVisible = _vm.CanHotReload; } private void PreviewVm_PropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName == nameof(PreviewVM.ImagePreview)) { Dispatcher.UIThread.Post(() => { var mat = _vm?.PreviewVm.ImagePreview; if (mat != null && !mat.Empty()) { PreviewImage.Source = ImageConverter.MatToAvaloniaBitmap(mat); } var isError = _vm?.PreviewVm.IsError ?? false; PreviewBorder.BorderThickness = isError ? new Thickness(4) : new Thickness(0); }); } } #region Statistics private void InitializeStatsFields() { StatsPanel.Children.Clear(); _statsFields.Clear(); UpdateStat(StartedAtLabel, ""); UpdateStat(ProcessingTimeLabel, ""); UpdateStat(TotalLabel, "0"); UpdateStat(BadLabel, "0"); UpdateStat(ErrorRateLabel, "0"); UpdateStat(DetailsLabel, ""); } private void StatisticsVm_PropertyChanged(object? sender, PropertyChangedEventArgs e) { Dispatcher.UIThread.Post(() => { if (_vm == null) return; var stats = _vm.StatisticsVm; switch (e.PropertyName) { case nameof(StatisticsVM.SessionStarted): InitializeStatsFields(); UpdateStat(StartedAtLabel, stats.SessionStarted); break; case nameof(StatisticsVM.ProcessingTime): UpdateStat(ProcessingTimeLabel, stats.ProcessingTime); break; case nameof(StatisticsVM.Bad): UpdateStat(BadLabel, stats.Bad.ToString()); break; case nameof(StatisticsVM.Total): UpdateStat(TotalLabel, stats.Total.ToString()); break; case nameof(StatisticsVM.ErrorRate): UpdateStat(ErrorRateLabel, stats.ErrorRate); break; case nameof(StatisticsVM.StatisticsDetails): UpdateStat(DetailsLabel, stats.StatisticsDetails); break; } }); } private void UpdateStat(string name, string text) { if (!_statsFields.ContainsKey(name)) { var label = new TextBlock { Text = name + ":", FontWeight = FontWeight.Bold, FontSize = 12, Margin = new Thickness(0, 6, 0, 0) }; var value = new TextBlock { Text = text, FontSize = 12, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0) }; StatsPanel.Children.Add(label); StatsPanel.Children.Add(value); _statsFields[name] = (label, value); } else { _statsFields[name].value.Text = text; } } #endregion #region Errors private void Errors_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { Dispatcher.UIThread.Post(() => { switch (e.Action) { case NotifyCollectionChangedAction.Add: if (e.NewItems != null) { foreach (ErrorData error in e.NewItems) AddErrorThumbnail(error); } break; case NotifyCollectionChangedAction.Remove: if (e.OldItems != null) { foreach (ErrorData error in e.OldItems) RemoveErrorThumbnail(error); } break; case NotifyCollectionChangedAction.Reset: ErrorsList.ItemsSource = null; _errorControls.Clear(); break; } }); } private readonly Dictionary _errorControls = new(); private void AddErrorThumbnail(ErrorData errorData) { var panel = new StackPanel { Width = 183, Margin = new Thickness(4), Cursor = new Cursor(StandardCursorType.Hand) }; Bitmap? thumbnail = null; try { if (errorData.ImageAnalysis != null && !errorData.ImageAnalysis.Empty()) { thumbnail = ImageConverter.MatToAvaloniaBitmap( errorData.ImageAnalysis.Resize(new OpenCvSharp.Size(183, 138))); } } catch { // Ignore conversion errors } var image = new Image { Source = thumbnail, Height = 138, Stretch = Stretch.Uniform }; var text = new TextBlock { Text = errorData.Title, FontSize = 11, HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Center, TextTrimming = TextTrimming.CharacterEllipsis }; panel.Children.Add(image); panel.Children.Add(text); panel.PointerPressed += (_, _) => { _vm?.ErrorsVm.ShowImage(errorData); }; _errorControls[errorData] = panel; // Insert at the beginning (newest first) if (ErrorsList.ItemsSource == null) { var items = new ObservableCollection(); ErrorsList.ItemsSource = items; } if (ErrorsList.ItemsSource is ObservableCollection collection) { collection.Insert(0, panel); } } private void RemoveErrorThumbnail(ErrorData errorData) { if (_errorControls.TryGetValue(errorData, out var control)) { if (ErrorsList.ItemsSource is ObservableCollection collection) { collection.Remove(control); } _errorControls.Remove(errorData); } } #endregion #region Dynamic Buttons private void DynamicButtons_CollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) { Dispatcher.UIThread.Post(() => { switch (e.Action) { case NotifyCollectionChangedAction.Add: foreach (ButtonDefinition btn in e.NewItems!) AddDynamicButton(btn); break; case NotifyCollectionChangedAction.Remove: foreach (ButtonDefinition btn in e.OldItems!) RemoveDynamicButton(btn); break; case NotifyCollectionChangedAction.Reset: foreach (var ctrl in _dynamicButtonControls.Values) ButtonsPanel.Children.Remove(ctrl); _dynamicButtonControls.Clear(); break; } }); } private void AddDynamicButton(ButtonDefinition buttonDef) { Control control; if (buttonDef.Type == EButtonType.Toggle) { var toggleBtn = new ToggleButton { Content = buttonDef.Title.ToUpperInvariant(), Height = 60, HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Stretch, HorizontalContentAlignment = global::Avalonia.Layout.HorizontalAlignment.Center, VerticalContentAlignment = global::Avalonia.Layout.VerticalAlignment.Center, FontSize = 14, FontWeight = FontWeight.Bold, IsChecked = buttonDef.IsToggled, IsVisible = buttonDef.IsVisible, BorderBrush = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)), BorderThickness = new Thickness(2), Background = Brushes.White, Foreground = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)) }; toggleBtn.Click += (_, _) => buttonDef.Execute(); buttonDef.PropertyChanged += (_, args) => { Dispatcher.UIThread.Post(() => { if (args.PropertyName == nameof(ButtonDefinition.IsVisible)) toggleBtn.IsVisible = buttonDef.IsVisible; else if (args.PropertyName == nameof(ButtonDefinition.IsToggled)) toggleBtn.IsChecked = buttonDef.IsToggled; }); }; control = toggleBtn; } else { var btn = new Button { Content = buttonDef.Title.ToUpperInvariant(), Height = 60, HorizontalAlignment = global::Avalonia.Layout.HorizontalAlignment.Stretch, HorizontalContentAlignment = global::Avalonia.Layout.HorizontalAlignment.Center, VerticalContentAlignment = global::Avalonia.Layout.VerticalAlignment.Center, FontSize = 14, FontWeight = FontWeight.Bold, IsVisible = buttonDef.IsVisible, BorderBrush = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)), BorderThickness = new Thickness(2), Background = Brushes.White, Foreground = new SolidColorBrush(Color.FromRgb(0xD3, 0x2F, 0x2F)) }; btn.Click += (_, _) => buttonDef.Execute(); buttonDef.PropertyChanged += (_, args) => { Dispatcher.UIThread.Post(() => { if (args.PropertyName == nameof(ButtonDefinition.IsVisible)) btn.IsVisible = buttonDef.IsVisible; }); }; control = btn; } ButtonsPanel.Children.Add(control); _dynamicButtonControls[buttonDef.Key] = control; } private void RemoveDynamicButton(ButtonDefinition buttonDef) { if (_dynamicButtonControls.TryGetValue(buttonDef.Key, out var ctrl)) { ButtonsPanel.Children.Remove(ctrl); _dynamicButtonControls.Remove(buttonDef.Key); } } #endregion }