From 129486ce89f3edd58845f145df41b840c867d66a Mon Sep 17 00:00:00 2001 From: Robert McRackan Date: Mon, 31 Aug 2026 17:15:56 -0400 Subject: [PATCH] Warn if accounts dialog is dirty when user attempts to close it. This includes editing items from the Marketplaces child-dialog --- .../Dialogs/AccountsDialog.axaml.cs | 55 ++++++++++++++++++- .../Dialogs/AccountsDialog.cs | 42 +++++++++++++- 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs b/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs index 6a86d50f..b1ea67cc 100644 --- a/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs +++ b/Source/LibationAvalonia/Dialogs/AccountsDialog.axaml.cs @@ -18,10 +18,16 @@ namespace LibationAvalonia.Dialogs; public partial class AccountsDialog : DialogWindow { public AvaloniaList Accounts { get; } = new(); + private bool _isDirty; + private bool _closeConfirmed; public class AccountDto : ViewModels.ViewModelBase { public IReadOnlyList Locales => AccountsDialog.Locales; - public bool LibraryScan { get; set; } = true; + public bool LibraryScan + { + get => field; + set => this.RaiseAndSetIfChanged(ref field, value); + } = true; public string? AccountId { get => field; @@ -43,7 +49,11 @@ public partial class AccountsDialog : DialogWindow } } - public string? AccountName { get; set; } + public string? AccountName + { + get => field; + set => this.RaiseAndSetIfChanged(ref field, value); + } public bool IsDefault => string.IsNullOrEmpty(AccountId); /// @@ -153,7 +163,11 @@ public partial class AccountsDialog : DialogWindow else if (e.Action is NotifyCollectionChangedAction.Remove && e.OldItems?.Count > 0) { foreach (var oldItem in e.OldItems.OfType()) + { oldItem.PropertyChanged -= AccountDto_PropertyChanged; + if (!oldItem.IsDefault) + _isDirty = true; + } } } @@ -161,6 +175,13 @@ public partial class AccountsDialog : DialogWindow private void AccountDto_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e) { + // only user-editable fields; skip derived props like CanExport / button tooltips + if (e.PropertyName is nameof(AccountDto.LibraryScan) + or nameof(AccountDto.AccountId) + or nameof(AccountDto.SelectedLocale) + or nameof(AccountDto.AccountName)) + _isDirty = true; + if (!Accounts.Any(a => a.IsDefault)) addBlankAccount(); } @@ -214,7 +235,10 @@ public partial class AccountsDialog : DialogWindow } if (importResult.Account is { } account) + { Accounts.Add(new AccountDto(account)); + _isDirty = true; + } } catch (Exception ex) { @@ -252,7 +276,10 @@ public partial class AccountsDialog : DialogWindow var dialog = new MarketplacesDialog(account, persister.AccountsSettings, acc.AdditionalLocaleNames); if (await dialog.ShowDialog(this) == DialogResult.OK) + { acc.SetAdditionalLocaleNames(dialog.SelectedAdditionalLocaleNames); + _isDirty = true; + } } protected override async Task SaveAndCloseAsync() @@ -282,6 +309,30 @@ public partial class AccountsDialog : DialogWindow public async void SaveButton_Clicked(object sender, Avalonia.Interactivity.RoutedEventArgs e) => await SaveAndCloseAsync(); + protected override async void OnClosing(WindowClosingEventArgs e) + { + if (!_closeConfirmed && _isDirty && DialogResult != DialogResult.OK) + { + e.Cancel = true; + + var result = await MessageBox.Show( + this, + "You have unsaved changes. Close without saving?", + "Unsaved Changes", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button2); + + if (result == DialogResult.Yes) + { + _closeConfirmed = true; + Close(DialogResult.Cancel); + } + } + + base.OnClosing(e); + } + private void persist(AccountsSettings accountsSettings) { var existingAccounts = accountsSettings.Accounts; diff --git a/Source/LibationWinForms/Dialogs/AccountsDialog.cs b/Source/LibationWinForms/Dialogs/AccountsDialog.cs index b37d3dc1..1f4e65e3 100644 --- a/Source/LibationWinForms/Dialogs/AccountsDialog.cs +++ b/Source/LibationWinForms/Dialogs/AccountsDialog.cs @@ -21,6 +21,8 @@ public partial class AccountsDialog : Form private const string COL_Locale = nameof(Locale); private const string COL_Marketplaces = nameof(Marketplaces); + private bool _isDirty; + public AccountsDialog() { InitializeComponent(); @@ -29,6 +31,7 @@ public partial class AccountsDialog : Form dataGridView1.CellValueChanged += DataGridView1_CellValueChanged; dataGridView1.CurrentCellDirtyStateChanged += DataGridView1_CurrentCellDirtyStateChanged; + dataGridView1.UserAddedRow += DataGridView1_UserAddedRow; populateDropDown(); @@ -36,6 +39,31 @@ public partial class AccountsDialog : Form this.SetLibationIcon(); } + protected override void OnFormClosing(FormClosingEventArgs e) + { + // commit in-progress edits so CellValueChanged can mark dirty before we decide + if (dataGridView1.IsCurrentCellInEditMode) + dataGridView1.EndEdit(); + if (dataGridView1.IsCurrentCellDirty) + dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit); + + if (_isDirty && DialogResult != DialogResult.OK) + { + var result = MessageBox.Show( + this, + "You have unsaved changes. Close without saving?", + "Unsaved Changes", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning, + MessageBoxDefaultButton.Button2); + + if (result == DialogResult.No) + e.Cancel = true; + } + + base.OnFormClosing(e); + } + private void populateDropDown() => (dataGridView1.Columns[COL_Locale] as DataGridViewComboBoxColumn)?.DataSource = Localization.Locales @@ -90,6 +118,7 @@ public partial class AccountsDialog : Form { if (e.RowIndex < 0 || e.ColumnIndex < 0) return; + _isDirty = true; var colName = dataGridView1.Columns[e.ColumnIndex].Name; if (colName is COL_AccountId or COL_Locale) UpdateExportCellState(dataGridView1.Rows[e.RowIndex]); @@ -100,10 +129,14 @@ public partial class AccountsDialog : Form if (!dataGridView1.IsCurrentCellDirty || dataGridView1.CurrentCell is null) return; var colName = dataGridView1.Columns[dataGridView1.CurrentCell.ColumnIndex].Name; - if (colName == COL_Locale) + // combo and checkbox do not commit until leave-cell unless we force it here + if (colName is COL_Locale or COL_LibraryScan) dataGridView1.CommitEdit(DataGridViewDataErrorContexts.Commit); } + private void DataGridView1_UserAddedRow(object? sender, DataGridViewRowEventArgs e) + => _isDirty = true; + private static bool AccountRowCanExport(string? accountId, string? localeName) { if (string.IsNullOrWhiteSpace(accountId) || string.IsNullOrWhiteSpace(localeName)) @@ -150,7 +183,10 @@ public partial class AccountsDialog : Form case COL_Delete: // if final/edit row: do nothing if (e.RowIndex < dgv.RowCount - 1) + { dgv.Rows.Remove(row); + _isDirty = true; + } break; case COL_Export: // if final/edit row: do nothing @@ -222,6 +258,7 @@ public partial class AccountsDialog : Form var selected = dialog.SelectedAdditionalLocaleNames.ToList(); row.Tag = selected; row.Cells[COL_Marketplaces].Value = MarketplacesUi.ButtonText(selected.Count + 1); + _isDirty = true; } private void saveBtn_Click(object sender, EventArgs e) @@ -416,7 +453,10 @@ public partial class AccountsDialog : Form } if (importResult.Account is { } account) + { AddAccountToGrid(account); + _isDirty = true; + } } catch (Exception ex) {