Files
HawkeyeVision/VisionBuilder.UI.Avalonia/Views/SingleCameraView.axaml.cs
2026-04-01 09:34:02 +02:00

416 lines
14 KiB
C#

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.Services;
using VisionBuilder.UI.Common;
using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.Common.ViewModel.Classes;
namespace VisionBuilder.UI.Avalonia.Views;
public partial class SingleCameraView : UserControl
{
private SingleCameraVM? _vm;
private ObservableCollection<ErrorData>? _subscribedErrors;
private readonly Dictionary<string, (TextBlock label, TextBlock value)> _statsFields = new();
private readonly Dictionary<string, Control> _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<ErrorData, Control> _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<Control>();
ErrorsList.ItemsSource = items;
}
if (ErrorsList.ItemsSource is ObservableCollection<Control> collection)
{
collection.Insert(0, panel);
}
}
private void RemoveErrorThumbnail(ErrorData errorData)
{
if (_errorControls.TryGetValue(errorData, out var control))
{
if (ErrorsList.ItemsSource is ObservableCollection<Control> 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
}