2026-05-09 17:53:15 +02:00
|
|
|
using Avalonia;
|
2026-02-26 18:43:07 +01:00
|
|
|
using Avalonia.Controls;
|
|
|
|
|
using Avalonia.Interactivity;
|
2026-05-09 17:53:15 +02:00
|
|
|
using Avalonia.Layout;
|
2026-02-26 18:43:07 +01:00
|
|
|
using Avalonia.Platform.Storage;
|
|
|
|
|
using Avalonia.Styling;
|
|
|
|
|
using Avalonia.Threading;
|
2026-05-09 20:27:44 +02:00
|
|
|
using ImageCatalog_2.Services;
|
|
|
|
|
using Microsoft.Extensions.DependencyInjection;
|
2026-05-09 12:09:05 +02:00
|
|
|
using System.ComponentModel;
|
2026-02-26 18:43:07 +01:00
|
|
|
using System.IO;
|
|
|
|
|
|
|
|
|
|
namespace ImageCatalog_2;
|
|
|
|
|
|
|
|
|
|
public partial class AvaloniaMainWindow : Window
|
|
|
|
|
{
|
|
|
|
|
private readonly DataModel _model;
|
2026-05-09 20:27:44 +02:00
|
|
|
private readonly PickerPreferenceService _pickerPreferenceService;
|
2026-02-28 21:48:05 +01:00
|
|
|
private bool _isDarkTheme;
|
2026-05-24 17:29:05 +02:00
|
|
|
private bool _startupSettingsRestoreAttempted;
|
2026-02-26 18:43:07 +01:00
|
|
|
|
2026-05-24 18:33:54 +02:00
|
|
|
public AvaloniaMainWindow()
|
|
|
|
|
: this(Program.ServiceProvider.GetRequiredService<DataModel>())
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-28 21:48:05 +01:00
|
|
|
public AvaloniaMainWindow(DataModel model)
|
2026-02-26 18:43:07 +01:00
|
|
|
{
|
|
|
|
|
InitializeComponent();
|
2026-02-28 21:48:05 +01:00
|
|
|
|
2026-02-26 18:43:07 +01:00
|
|
|
_model = model;
|
2026-05-09 20:27:44 +02:00
|
|
|
_pickerPreferenceService = Program.ServiceProvider.GetRequiredService<PickerPreferenceService>();
|
2026-02-26 18:43:07 +01:00
|
|
|
DataContext = _model;
|
|
|
|
|
|
2026-05-24 17:29:05 +02:00
|
|
|
Opened += async (_, _) =>
|
|
|
|
|
{
|
|
|
|
|
SyncThemeStateFromCurrentTheme();
|
|
|
|
|
await TryLoadLastSettingsOnStartupAsync();
|
|
|
|
|
};
|
2026-05-09 12:09:05 +02:00
|
|
|
Closing += AvaloniaMainWindow_Closing;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
2026-02-28 21:48:05 +01:00
|
|
|
// Let DataModel marshal callbacks onto Avalonia UI thread.
|
2026-02-26 18:43:07 +01:00
|
|
|
_model.UiInvoker = action => Dispatcher.UIThread.Invoke(action);
|
2026-05-24 18:45:51 +02:00
|
|
|
_model.ConfirmAiCsvOverwriteAsync = ShowConfirmationDialogAsync;
|
2026-02-26 18:43:07 +01:00
|
|
|
|
|
|
|
|
_model.SelectSourceFolderRequested += async (_, _) =>
|
|
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.SourceFolder, _model.SourcePath);
|
2026-02-28 21:48:05 +01:00
|
|
|
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
|
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
Title = "Seleziona cartella sorgente",
|
|
|
|
|
SuggestedStartLocation = suggestedStartLocation
|
2026-02-28 21:48:05 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (folders.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
_model.SourcePath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
2026-05-09 20:27:44 +02:00
|
|
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.SourceFolder, _model.SourcePath);
|
2026-02-28 21:48:05 +01:00
|
|
|
}
|
2026-02-26 18:43:07 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.SelectDestinationFolderRequested += async (_, _) =>
|
|
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.DestinationFolder, _model.DestinationPath);
|
2026-02-28 21:48:05 +01:00
|
|
|
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
|
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
Title = "Seleziona cartella destinazione",
|
|
|
|
|
SuggestedStartLocation = suggestedStartLocation
|
2026-02-28 21:48:05 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (folders.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
_model.DestinationPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
2026-05-09 20:27:44 +02:00
|
|
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.DestinationFolder, _model.DestinationPath);
|
2026-02-28 21:48:05 +01:00
|
|
|
}
|
2026-02-26 18:43:07 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.SelectLogoFileRequested += async (_, _) =>
|
|
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.LogoFile, _model.LogoFile);
|
2026-02-26 18:43:07 +01:00
|
|
|
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
|
|
|
|
{
|
|
|
|
|
Title = "Seleziona logo",
|
2026-05-09 20:27:44 +02:00
|
|
|
SuggestedStartLocation = suggestedStartLocation,
|
2026-02-28 21:48:05 +01:00
|
|
|
FileTypeFilter =
|
|
|
|
|
[
|
|
|
|
|
new FilePickerFileType("Immagini") { Patterns = ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif"] }
|
|
|
|
|
]
|
2026-02-26 18:43:07 +01:00
|
|
|
});
|
2026-02-28 21:48:05 +01:00
|
|
|
|
2026-02-26 18:43:07 +01:00
|
|
|
if (files.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
_model.LogoFile = files[0].Path.LocalPath;
|
2026-05-09 20:27:44 +02:00
|
|
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.LogoFile, _model.LogoFile);
|
2026-02-26 18:43:07 +01:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.SelectModelsFolderRequested += async (_, _) =>
|
|
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.ModelsFolder, _model.ModelsFolderPath);
|
2026-02-28 21:48:05 +01:00
|
|
|
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
|
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
Title = "Seleziona cartella modelli",
|
|
|
|
|
SuggestedStartLocation = suggestedStartLocation
|
2026-02-28 21:48:05 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (folders.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
_model.ModelsFolderPath = folders[0].Path.LocalPath + Path.DirectorySeparatorChar;
|
2026-05-09 20:27:44 +02:00
|
|
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.ModelsFolder, _model.ModelsFolderPath);
|
2026-02-28 21:48:05 +01:00
|
|
|
}
|
2026-02-26 18:43:07 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.SelectCsvOutputRequested += async (_, _) =>
|
|
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.CsvOutput, _model.CsvOutputPath);
|
2026-02-26 18:43:07 +01:00
|
|
|
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
|
|
|
|
{
|
|
|
|
|
Title = "Salva CSV",
|
|
|
|
|
DefaultExtension = "csv",
|
2026-05-09 20:27:44 +02:00
|
|
|
FileTypeChoices = [new FilePickerFileType("CSV") { Patterns = ["*.csv"] }],
|
|
|
|
|
SuggestedStartLocation = suggestedStartLocation
|
2026-02-26 18:43:07 +01:00
|
|
|
});
|
2026-02-28 21:48:05 +01:00
|
|
|
|
|
|
|
|
if (file is not null)
|
|
|
|
|
{
|
|
|
|
|
_model.CsvOutputPath = file.Path.LocalPath;
|
2026-05-09 20:27:44 +02:00
|
|
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.CsvOutput, _model.CsvOutputPath);
|
2026-02-28 21:48:05 +01:00
|
|
|
}
|
2026-02-26 18:43:07 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.SaveSettingsRequested += async (_, _) =>
|
|
|
|
|
{
|
2026-05-24 17:29:05 +02:00
|
|
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.SettingsFile);
|
2026-02-26 18:43:07 +01:00
|
|
|
var file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
|
|
|
|
{
|
|
|
|
|
Title = "Salva impostazioni",
|
|
|
|
|
DefaultExtension = "xml",
|
2026-05-24 17:29:05 +02:00
|
|
|
FileTypeChoices = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }],
|
|
|
|
|
SuggestedStartLocation = suggestedStartLocation
|
2026-02-26 18:43:07 +01:00
|
|
|
});
|
2026-02-28 21:48:05 +01:00
|
|
|
|
|
|
|
|
if (file is not null)
|
|
|
|
|
{
|
|
|
|
|
await _model.SaveSettingsToFileAsync(file.Path.LocalPath);
|
2026-05-24 17:29:05 +02:00
|
|
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.SettingsFile, file.Path.LocalPath);
|
|
|
|
|
_pickerPreferenceService.RememberValue(PickerPreferenceKeys.LastSettingsFile, file.Path.LocalPath);
|
2026-02-28 21:48:05 +01:00
|
|
|
}
|
2026-02-26 18:43:07 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.LoadSettingsRequested += async (_, _) =>
|
|
|
|
|
{
|
2026-05-24 17:29:05 +02:00
|
|
|
var suggestedStartLocation = await _pickerPreferenceService.TryGetStartFolderAsync(StorageProvider, PickerPreferenceKeys.SettingsFile);
|
2026-02-26 18:43:07 +01:00
|
|
|
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
|
|
|
|
{
|
|
|
|
|
Title = "Carica impostazioni",
|
2026-05-24 17:29:05 +02:00
|
|
|
FileTypeFilter = [new FilePickerFileType("Setup") { Patterns = ["*.xml"] }],
|
|
|
|
|
SuggestedStartLocation = suggestedStartLocation
|
2026-02-26 18:43:07 +01:00
|
|
|
});
|
2026-02-28 21:48:05 +01:00
|
|
|
|
|
|
|
|
if (files.Count > 0)
|
|
|
|
|
{
|
|
|
|
|
await _model.LoadSettingsFromFileAsync(files[0].Path.LocalPath);
|
2026-05-24 17:29:05 +02:00
|
|
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.SettingsFile, files[0].Path.LocalPath);
|
|
|
|
|
_pickerPreferenceService.RememberValue(PickerPreferenceKeys.LastSettingsFile, files[0].Path.LocalPath);
|
2026-02-28 21:48:05 +01:00
|
|
|
}
|
2026-02-26 18:43:07 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.SelectColorRequested += (_, _) =>
|
|
|
|
|
{
|
2026-02-28 21:48:05 +01:00
|
|
|
// Color is set by typing hex directly in the TextBox.
|
2026-02-26 18:43:07 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
_model.SelectTransparentColorRequested += (_, _) =>
|
|
|
|
|
{
|
2026-02-28 21:48:05 +01:00
|
|
|
// Color is set by typing hex directly in the TextBox.
|
2026-02-26 18:43:07 +01:00
|
|
|
};
|
2026-05-09 17:53:15 +02:00
|
|
|
|
|
|
|
|
_model.ShowMessageRequested += async (_, args) =>
|
|
|
|
|
{
|
|
|
|
|
await ShowMessageDialogAsync(args.Item1, args.Item2);
|
|
|
|
|
};
|
2026-02-26 18:43:07 +01:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 12:09:05 +02:00
|
|
|
private bool _isStoppingFaceEncoderForClose;
|
|
|
|
|
|
2026-05-24 17:29:05 +02:00
|
|
|
private async Task TryLoadLastSettingsOnStartupAsync()
|
|
|
|
|
{
|
|
|
|
|
if (_startupSettingsRestoreAttempted)
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_startupSettingsRestoreAttempted = true;
|
|
|
|
|
|
|
|
|
|
var lastSettingsFile = _pickerPreferenceService.GetRememberedValue(PickerPreferenceKeys.LastSettingsFile);
|
|
|
|
|
if (string.IsNullOrWhiteSpace(lastSettingsFile))
|
|
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!File.Exists(lastSettingsFile))
|
|
|
|
|
{
|
|
|
|
|
_pickerPreferenceService.ForgetValue(PickerPreferenceKeys.LastSettingsFile);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
await _model.LoadSettingsFromFileAsync(lastSettingsFile);
|
|
|
|
|
_pickerPreferenceService.RememberPath(PickerPreferenceKeys.SettingsFile, lastSettingsFile);
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
await ShowMessageDialogAsync("Impostazioni", $"Impossibile caricare il file impostazioni automatico:\n{ex.GetBaseException().Message}");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 12:09:05 +02:00
|
|
|
private async void AvaloniaMainWindow_Closing(object? sender, CancelEventArgs e)
|
|
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
if (_isStoppingFaceEncoderForClose || (!_model.IsFaceEncoderRunning && !_model.IsFaceMatcherRunning))
|
2026-05-09 12:09:05 +02:00
|
|
|
{
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
e.Cancel = true;
|
|
|
|
|
_isStoppingFaceEncoderForClose = true;
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
{
|
2026-05-09 20:27:44 +02:00
|
|
|
if (_model.IsFaceMatcherRunning)
|
|
|
|
|
{
|
|
|
|
|
await _model.StopFaceMatcherAsync("Arresto face matcher in chiusura...", waitForExit: true);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (_model.IsFaceEncoderRunning)
|
|
|
|
|
{
|
|
|
|
|
await _model.StopFaceEncoderAsync("Arresto face encoder in chiusura...", waitForExit: true);
|
|
|
|
|
}
|
2026-05-09 12:09:05 +02:00
|
|
|
}
|
|
|
|
|
finally
|
|
|
|
|
{
|
|
|
|
|
_isStoppingFaceEncoderForClose = false;
|
|
|
|
|
Close();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-26 18:43:07 +01:00
|
|
|
private void ToggleTheme_Click(object? sender, RoutedEventArgs e)
|
|
|
|
|
{
|
|
|
|
|
_isDarkTheme = !_isDarkTheme;
|
2026-02-28 21:34:45 +01:00
|
|
|
|
2026-02-28 21:48:05 +01:00
|
|
|
if (Avalonia.Application.Current is not null)
|
2026-02-28 21:34:45 +01:00
|
|
|
{
|
2026-02-28 21:48:05 +01:00
|
|
|
Avalonia.Application.Current.RequestedThemeVariant = _isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light;
|
2026-02-28 21:34:45 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-28 21:48:05 +01:00
|
|
|
UpdateThemeToggleButtonContent();
|
2026-02-28 21:34:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void SyncThemeStateFromCurrentTheme()
|
|
|
|
|
{
|
2026-02-28 21:48:05 +01:00
|
|
|
_isDarkTheme = ActualThemeVariant == ThemeVariant.Dark;
|
2026-02-28 21:34:45 +01:00
|
|
|
UpdateThemeToggleButtonContent();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private void UpdateThemeToggleButtonContent()
|
|
|
|
|
{
|
2026-03-12 23:40:41 +01:00
|
|
|
_ = this.FindControl<Avalonia.Controls.Button>("ThemeToggleButton");
|
2026-02-28 21:34:45 +01:00
|
|
|
}
|
2026-05-09 17:53:15 +02:00
|
|
|
|
|
|
|
|
private async Task ShowMessageDialogAsync(string title, string message)
|
|
|
|
|
{
|
|
|
|
|
var dialog = new Window
|
|
|
|
|
{
|
|
|
|
|
Title = title,
|
|
|
|
|
Width = 480,
|
|
|
|
|
CanResize = false,
|
|
|
|
|
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
|
|
|
SizeToContent = SizeToContent.Height
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
dialog.Content = BuildMessageDialogContent(message, () => dialog.Close());
|
|
|
|
|
|
|
|
|
|
await dialog.ShowDialog(this);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-24 18:45:51 +02:00
|
|
|
private async Task<bool> ShowConfirmationDialogAsync(string title, string message)
|
|
|
|
|
{
|
|
|
|
|
var dialog = new Window
|
|
|
|
|
{
|
|
|
|
|
Title = title,
|
|
|
|
|
Width = 520,
|
|
|
|
|
CanResize = false,
|
|
|
|
|
WindowStartupLocation = WindowStartupLocation.CenterOwner,
|
|
|
|
|
SizeToContent = SizeToContent.Height
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
dialog.Content = BuildConfirmationDialogContent(
|
|
|
|
|
message,
|
|
|
|
|
() => dialog.Close(true),
|
|
|
|
|
() => dialog.Close(false));
|
|
|
|
|
|
|
|
|
|
return await dialog.ShowDialog<bool>(this);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 17:53:15 +02:00
|
|
|
private static Control BuildMessageDialogContent(string message, Action closeDialog)
|
|
|
|
|
{
|
|
|
|
|
var layout = new StackPanel
|
|
|
|
|
{
|
|
|
|
|
Margin = new Thickness(16),
|
|
|
|
|
Spacing = 12
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
layout.Children.Add(new TextBlock
|
|
|
|
|
{
|
|
|
|
|
Text = message,
|
|
|
|
|
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
|
|
|
|
|
MaxWidth = 420
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var closeButton = new Button
|
|
|
|
|
{
|
|
|
|
|
Content = "OK",
|
|
|
|
|
MinWidth = 88,
|
|
|
|
|
HorizontalAlignment = HorizontalAlignment.Right
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
closeButton.Click += (_, _) => closeDialog();
|
|
|
|
|
|
|
|
|
|
layout.Children.Add(closeButton);
|
|
|
|
|
return layout;
|
|
|
|
|
}
|
2026-05-24 18:45:51 +02:00
|
|
|
|
|
|
|
|
private static Control BuildConfirmationDialogContent(string message, Action confirmDialog, Action cancelDialog)
|
|
|
|
|
{
|
|
|
|
|
var layout = new StackPanel
|
|
|
|
|
{
|
|
|
|
|
Margin = new Thickness(16),
|
|
|
|
|
Spacing = 12
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
layout.Children.Add(new TextBlock
|
|
|
|
|
{
|
|
|
|
|
Text = message,
|
|
|
|
|
TextWrapping = Avalonia.Media.TextWrapping.Wrap,
|
|
|
|
|
MaxWidth = 460
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var buttons = new StackPanel
|
|
|
|
|
{
|
|
|
|
|
Orientation = Orientation.Horizontal,
|
|
|
|
|
HorizontalAlignment = HorizontalAlignment.Right,
|
|
|
|
|
Spacing = 8
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var cancelButton = new Button
|
|
|
|
|
{
|
|
|
|
|
Content = "Annulla",
|
|
|
|
|
MinWidth = 96
|
|
|
|
|
};
|
|
|
|
|
cancelButton.Click += (_, _) => cancelDialog();
|
|
|
|
|
|
|
|
|
|
var confirmButton = new Button
|
|
|
|
|
{
|
|
|
|
|
Content = "Sovrascrivi",
|
|
|
|
|
MinWidth = 96
|
|
|
|
|
};
|
|
|
|
|
confirmButton.Click += (_, _) => confirmDialog();
|
|
|
|
|
|
|
|
|
|
buttons.Children.Add(cancelButton);
|
|
|
|
|
buttons.Children.Add(confirmButton);
|
|
|
|
|
layout.Children.Add(buttons);
|
|
|
|
|
return layout;
|
|
|
|
|
}
|
2026-02-26 18:43:07 +01:00
|
|
|
}
|