avalonia structure refactoring

This commit is contained in:
EugeneTes
2026-04-01 09:34:02 +02:00
parent 1c7a4abd5c
commit 1032a5a28c
24 changed files with 65 additions and 31 deletions

View File

@@ -0,0 +1,87 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="VisionBuilder.UI.Avalonia.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>

View File

@@ -0,0 +1,74 @@
using System.ComponentModel;
using Avalonia.Controls;
using VisionBuilder.UI.Avalonia.Services;
using VisionBuilder.UI.Common.ViewModel;
namespace VisionBuilder.UI.Avalonia.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;
}
}
}

View File

@@ -0,0 +1,34 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="VisionBuilder.UI.Avalonia.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>

View File

@@ -0,0 +1,23 @@
using Avalonia.Controls;
using Avalonia.Input;
namespace VisionBuilder.UI.Avalonia.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);
};
}
}

View File

@@ -0,0 +1,49 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="VisionBuilder.UI.Avalonia.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>

View File

@@ -0,0 +1,130 @@
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using VisionBuilder.UI.Avalonia.Services;
using VisionBuilder.UI.Common.ViewModel;
using VisionBuilder.UI.Common.ViewModel.Classes;
using VisionBuilder.UI.Common.ViewModel.Interfaces.UI;
namespace VisionBuilder.UI.Avalonia.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);
}

View File

@@ -0,0 +1,126 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:converters="using:VisionBuilder.UI.Avalonia.Converters"
x:Class="VisionBuilder.UI.Avalonia.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>

View File

@@ -0,0 +1,415 @@
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
}