avalonia structure refactoring
This commit is contained in:
@@ -1,87 +0,0 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="VisionBuilder.UI.Avalonia.Uno.Views.ImagePreviewDialog"
|
||||
Title="Image Preview"
|
||||
WindowState="Maximized"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="White"
|
||||
CanResize="True">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,*" Margin="16">
|
||||
<!-- Row 0: Error name label -->
|
||||
<TextBlock x:Name="LblImageName"
|
||||
Grid.Row="0"
|
||||
HorizontalAlignment="Center"
|
||||
FontSize="16"
|
||||
Margin="0,8,0,0" />
|
||||
|
||||
<!-- Row 1: Toolbar buttons -->
|
||||
<Grid Grid.Row="1" Margin="0,8,0,8">
|
||||
<!-- Left-aligned buttons -->
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Left"
|
||||
Spacing="8">
|
||||
<Button x:Name="BtnPrev"
|
||||
Content="PREVIOUS IMAGE"
|
||||
Width="168" Height="64"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnSwitchView"
|
||||
Content="SHOW ORIGINAL"
|
||||
Width="168" Height="64"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right-aligned buttons -->
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8">
|
||||
<Button x:Name="BtnLearn"
|
||||
Content="LEARN THIS"
|
||||
Width="168" Height="64"
|
||||
IsVisible="False"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnNext"
|
||||
Content="NEXT IMAGE"
|
||||
Width="168" Height="64"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnClose"
|
||||
Content="CLOSE"
|
||||
Width="168" Height="64"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Background="#D32F2F"
|
||||
Foreground="White" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Row 2: Image -->
|
||||
<Image x:Name="PreviewImage"
|
||||
Grid.Row="2"
|
||||
Stretch="Uniform" />
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,74 +0,0 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia.Controls;
|
||||
using VisionBuilder.UI.Avalonia.Uno.Services;
|
||||
using VisionBuilder.UI.Common.ViewModel;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Uno.Views;
|
||||
|
||||
public partial class ImagePreviewDialog : Window
|
||||
{
|
||||
private readonly ErrorPreviewVM _previewVm;
|
||||
|
||||
public ImagePreviewDialog(ErrorPreviewVM errorPreviewVm)
|
||||
{
|
||||
_previewVm = errorPreviewVm;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
_previewVm.PropertyChanged += PreviewVm_PropertyChanged;
|
||||
|
||||
// Initial state
|
||||
UpdateImage();
|
||||
BtnLearn.IsVisible = _previewVm.LearnButtonVisible;
|
||||
BtnPrev.IsEnabled = _previewVm.IsPreviousImageEnabled;
|
||||
BtnNext.IsEnabled = _previewVm.IsNextImageEnabled;
|
||||
LblImageName.Text = _previewVm.ImageName;
|
||||
|
||||
// Wire button events (direct method calls like WinForms)
|
||||
BtnPrev.Click += (_, _) => _previewVm.PreviousImage();
|
||||
BtnNext.Click += (_, _) => _previewVm.NextImage();
|
||||
BtnSwitchView.Click += (_, _) => _previewVm.SwitchView();
|
||||
BtnLearn.Click += (_, _) => _previewVm.Learn();
|
||||
BtnClose.Click += (_, _) => Close();
|
||||
}
|
||||
|
||||
private void PreviewVm_PropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
global::Avalonia.Threading.Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
switch (e.PropertyName)
|
||||
{
|
||||
case nameof(ErrorPreviewVM.ImageAnalysis):
|
||||
UpdateImage();
|
||||
break;
|
||||
case nameof(ErrorPreviewVM.LearnButtonVisible):
|
||||
BtnLearn.IsVisible = _previewVm.LearnButtonVisible;
|
||||
break;
|
||||
case nameof(ErrorPreviewVM.IsPreviousImageEnabled):
|
||||
BtnPrev.IsEnabled = _previewVm.IsPreviousImageEnabled;
|
||||
break;
|
||||
case nameof(ErrorPreviewVM.IsNextImageEnabled):
|
||||
BtnNext.IsEnabled = _previewVm.IsNextImageEnabled;
|
||||
break;
|
||||
case nameof(ErrorPreviewVM.ImageName):
|
||||
LblImageName.Text = _previewVm.ImageName;
|
||||
break;
|
||||
case nameof(ErrorPreviewVM.OriginalView):
|
||||
BtnSwitchView.Content = _previewVm.OriginalView ? "SHOW ANALYSIS" : "SHOW ORIGINAL";
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void UpdateImage()
|
||||
{
|
||||
if (_previewVm.ImageAnalysis != null && !_previewVm.ImageAnalysis.Empty())
|
||||
{
|
||||
PreviewImage.Source = ImageConverter.MatToAvaloniaBitmap(_previewVm.ImageAnalysis);
|
||||
}
|
||||
else
|
||||
{
|
||||
PreviewImage.Source = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:views="using:VisionBuilder.UI.Avalonia.Uno.Views"
|
||||
xmlns:views="using:VisionBuilder.UI.Avalonia.Views"
|
||||
x:Class="VisionBuilder.UI.Avalonia.Uno.Views.MainWindow"
|
||||
Title="VisionBuilder UNO"
|
||||
Width="1920" Height="1080"
|
||||
|
||||
@@ -7,6 +7,7 @@ using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using VisionBuilder.UI.Avalonia;
|
||||
using VisionBuilder.UI.Common.ViewModel;
|
||||
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="VisionBuilder.UI.Avalonia.Uno.Views.PasswordDialog"
|
||||
Title="Password Required"
|
||||
Width="400" Height="180"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
|
||||
<Grid RowDefinitions="Auto,Auto,Auto" Margin="16">
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="Enter password:"
|
||||
FontSize="14"
|
||||
Margin="0,0,0,8" />
|
||||
|
||||
<TextBox Grid.Row="1"
|
||||
x:Name="TxtPassword"
|
||||
PasswordChar="*"
|
||||
Margin="0,0,0,16" />
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8">
|
||||
<Button x:Name="BtnOk"
|
||||
Content="OK"
|
||||
Width="80" Height="36"
|
||||
HorizontalContentAlignment="Center" />
|
||||
<Button x:Name="BtnCancel"
|
||||
Content="Cancel"
|
||||
Width="80" Height="36"
|
||||
HorizontalContentAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,23 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Uno.Views;
|
||||
|
||||
public partial class PasswordDialog : Window
|
||||
{
|
||||
public PasswordDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
BtnOk.Click += (_, _) => Close(TxtPassword.Text);
|
||||
BtnCancel.Click += (_, _) => Close(null);
|
||||
|
||||
TxtPassword.KeyDown += (_, e) =>
|
||||
{
|
||||
if (e.Key == Key.Enter)
|
||||
Close(TxtPassword.Text);
|
||||
else if (e.Key == Key.Escape)
|
||||
Close(null);
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="VisionBuilder.UI.Avalonia.Uno.Views.RecipeSelectionDialog"
|
||||
Title="Select Recipe"
|
||||
Width="800" Height="600"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Background="White">
|
||||
|
||||
<Grid RowDefinitions="*,Auto">
|
||||
<ListBox x:Name="RecipeList"
|
||||
Grid.Row="0"
|
||||
Margin="8"
|
||||
Background="White"
|
||||
SelectionMode="Single">
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
</ListBox>
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="8" Spacing="8">
|
||||
<Button x:Name="BtnCreateNew"
|
||||
Content="CREATE NEW RECIPE"
|
||||
IsVisible="False"
|
||||
Height="48" Padding="16,0"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnCancel"
|
||||
Content="CANCEL"
|
||||
Height="48" Width="176" Padding="16,0"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -1,130 +0,0 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using VisionBuilder.UI.Avalonia.Uno.Services;
|
||||
using VisionBuilder.UI.Common.ViewModel;
|
||||
using VisionBuilder.UI.Common.ViewModel.Classes;
|
||||
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
|
||||
|
||||
namespace VisionBuilder.UI.Avalonia.Uno.Views;
|
||||
|
||||
public partial class RecipeSelectionDialog : global::Avalonia.Controls.Window
|
||||
{
|
||||
private readonly RecipeSelectionVM _recipeSelectionVm;
|
||||
private readonly IRecipeCreationTool _recipeCreationTool;
|
||||
private readonly TaskCompletionSource<bool> _tcs;
|
||||
|
||||
public RecipeSelectionDialog(RecipeSelectionVM recipeSelectionVm, IRecipeCreationTool recipeCreationTool, TaskCompletionSource<bool> tcs)
|
||||
{
|
||||
_recipeSelectionVm = recipeSelectionVm;
|
||||
_recipeCreationTool = recipeCreationTool;
|
||||
_tcs = tcs;
|
||||
|
||||
InitializeComponent();
|
||||
|
||||
if (_recipeCreationTool.Enabled)
|
||||
{
|
||||
BtnCreateNew.IsVisible = true;
|
||||
}
|
||||
|
||||
BtnCancel.Click += (_, _) =>
|
||||
{
|
||||
_tcs.TrySetResult(false);
|
||||
Close();
|
||||
};
|
||||
BtnCreateNew.Click += BtnCreateNew_Click;
|
||||
|
||||
Closed += (_, _) => _tcs.TrySetResult(false);
|
||||
|
||||
LoadRecipes();
|
||||
}
|
||||
|
||||
private void LoadRecipes()
|
||||
{
|
||||
var recipes = _recipeSelectionVm.Recipes.OrderBy(x => x.RecipeName).ToList();
|
||||
var items = new List<RecipeItem>();
|
||||
|
||||
foreach (var recipe in recipes)
|
||||
{
|
||||
Bitmap? thumbnail = null;
|
||||
try
|
||||
{
|
||||
if (recipe.Image != null && !recipe.Image.Empty())
|
||||
{
|
||||
thumbnail = ImageConverter.MatToAvaloniaBitmap(
|
||||
recipe.Image.Resize(new OpenCvSharp.Size(128, 128)));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore conversion errors
|
||||
}
|
||||
|
||||
items.Add(new RecipeItem(recipe, thumbnail));
|
||||
}
|
||||
|
||||
RecipeList.ItemsSource = items;
|
||||
|
||||
RecipeList.ItemTemplate = new global::Avalonia.Controls.Templates.FuncDataTemplate<RecipeItem>((item, _) =>
|
||||
{
|
||||
var panel = new StackPanel
|
||||
{
|
||||
Width = 140,
|
||||
Margin = new global::Avalonia.Thickness(4),
|
||||
HorizontalAlignment = HorizontalAlignment.Center
|
||||
};
|
||||
|
||||
var image = new Image
|
||||
{
|
||||
Source = item.Thumbnail,
|
||||
Width = 128,
|
||||
Height = 128,
|
||||
Stretch = Stretch.Uniform
|
||||
};
|
||||
|
||||
if (item.Thumbnail == null)
|
||||
{
|
||||
image.Source = null;
|
||||
}
|
||||
|
||||
var text = new TextBlock
|
||||
{
|
||||
Text = item.Recipe.RecipeName,
|
||||
HorizontalAlignment = HorizontalAlignment.Center,
|
||||
TextTrimming = global::Avalonia.Media.TextTrimming.CharacterEllipsis,
|
||||
FontSize = 12,
|
||||
Margin = new global::Avalonia.Thickness(0, 4, 0, 0)
|
||||
};
|
||||
|
||||
panel.Children.Add(image);
|
||||
panel.Children.Add(text);
|
||||
return panel;
|
||||
});
|
||||
|
||||
RecipeList.SelectionChanged += RecipeList_SelectionChanged;
|
||||
}
|
||||
|
||||
private void RecipeList_SelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (RecipeList.SelectedItem is RecipeItem item)
|
||||
{
|
||||
_recipeSelectionVm.SelectedRecipe = item.Recipe;
|
||||
_tcs.TrySetResult(true);
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private async void BtnCreateNew_Click(object? sender, global::Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
var recipeName = await _recipeCreationTool.CreateRecipeAsync();
|
||||
if (recipeName != null)
|
||||
{
|
||||
_recipeSelectionVm.SelectedRecipe = new RecipeData { RecipeName = recipeName };
|
||||
_tcs.TrySetResult(true);
|
||||
Close();
|
||||
}
|
||||
}
|
||||
|
||||
private record RecipeItem(RecipeData Recipe, Bitmap? Thumbnail);
|
||||
}
|
||||
@@ -1,126 +0,0 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:converters="using:VisionBuilder.UI.Avalonia.Uno.Converters"
|
||||
x:Class="VisionBuilder.UI.Avalonia.Uno.Views.SingleCameraView"
|
||||
Background="White">
|
||||
|
||||
<UserControl.Resources>
|
||||
<converters:MatToBitmapConverter x:Key="MatToBitmapConverter" />
|
||||
<converters:BoolToVisibilityConverter x:Key="BoolToVis" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid ColumnDefinitions="5*,1,2*,1,Auto,Auto">
|
||||
<!-- Column 0: Preview -->
|
||||
<Grid Grid.Column="0" RowDefinitions="Auto,*">
|
||||
<TextBlock Grid.Row="0"
|
||||
x:Name="LblCameraName"
|
||||
Text="Camera"
|
||||
FontSize="16" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,8,0,4" />
|
||||
|
||||
<Border Grid.Row="1" Margin="4"
|
||||
x:Name="PreviewBorder"
|
||||
BorderThickness="0"
|
||||
BorderBrush="Red">
|
||||
<Image x:Name="PreviewImage"
|
||||
Stretch="Uniform" />
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Column 1: Divider -->
|
||||
<Border Grid.Column="1" Background="#D32F2F" />
|
||||
|
||||
<!-- Column 2: Errors -->
|
||||
<Grid Grid.Column="2" RowDefinitions="Auto,*">
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="Errors"
|
||||
FontSize="16"
|
||||
HorizontalAlignment="Center"
|
||||
Margin="0,8,0,4" />
|
||||
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto">
|
||||
<ItemsControl x:Name="ErrorsList">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
|
||||
<!-- Column 3: Divider -->
|
||||
<Border Grid.Column="3" Background="#D32F2F" />
|
||||
|
||||
<!-- Column 4: Stats -->
|
||||
<ScrollViewer Grid.Column="4" Width="220" VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel x:Name="StatsPanel" Margin="8,8,8,0" Spacing="2" />
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Column 5: Action buttons (right side) -->
|
||||
<StackPanel x:Name="ButtonsPanel" Grid.Column="5" Width="180" Margin="8" Spacing="6"
|
||||
VerticalAlignment="Top">
|
||||
<Button x:Name="BtnSelectRecipe"
|
||||
Content="SELECT RECIPE"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnStart"
|
||||
Content="START"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
Background="#D32F2F" Foreground="White" />
|
||||
<Button x:Name="BtnStop"
|
||||
Content="STOP"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
Background="#D32F2F" Foreground="White" />
|
||||
<Button x:Name="BtnPause"
|
||||
Content="PAUSE"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnResume"
|
||||
Content="RESUME"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
<Button x:Name="BtnHotReload"
|
||||
Content="HOT RELOAD"
|
||||
Height="60"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
FontSize="14" FontWeight="Bold"
|
||||
BorderBrush="#D32F2F"
|
||||
BorderThickness="2"
|
||||
Background="White"
|
||||
Foreground="#D32F2F" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -1,415 +0,0 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user