416 lines
14 KiB
C#
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.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<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
|
|
}
|