diff options
| -rw-r--r-- | src/mainwindow.cpp | 27 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 38 |
2 files changed, 65 insertions, 0 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 0131959..9a94a2e 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -422,6 +422,33 @@ void MainWindow::closeEvent(QCloseEvent *event) } } + // Every composer goes with the window, and this is the LAST thing before + // the close is accepted: every route that turns back (Cancel, a failed + // sync, a refused save) has already returned above, so reaching here means + // the application really is quitting. + // + // A composer is deliberately parentless, so that it appears in the task + // switcher and stays usable while the main window is. Qt therefore does not + // take it down with this window, and it kept the process alive: the main + // window vanished, the composer stayed on screen with nothing behind it, + // and closing it then raised the unsaved-edits dialog for a session that + // had already ended. + // + // Closing rather than deleting. WA_DeleteOnClose is set on every composer, + // so close() is what frees them, and it lets ComposeWindow::closeEvent() + // run its own draft handling on the way out. The drafts have already been + // saved by the dialogs above, so that pass has nothing left to do; going + // through it anyway keeps ONE exit path rather than a second one that has + // to be kept in step. + // + // Iterating a COPY: closing a composer runs the `closed` handler, which + // mutates m_composers, and mutating a container mid-iteration is undefined. + const QList<QPointer<ComposeWindow>> composers = m_composers; + for (const QPointer<ComposeWindow> &composer : composers) { + if (composer) + composer->close(); + } + saveUiState(); QMainWindow::closeEvent(event); } diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index ecaab2f..98dae12 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -267,6 +267,7 @@ private slots: void savingAMessageWithAHostileSubjectStaysInTheDirectory(); void aStuckComposeRequestDoesNotHijackTheNextPaneLoad(); void theSaveLoopToleratesAComposerClosedUnderTheDialog(); + void quittingClosesEveryComposerRatherThanOrphaningIt(); void forwardingCarriesTheOriginalsAttachments(); void forwardSeedsHtmlFromTheConfigNotTheOriginal(); void aStartupAccountScopesTheStartupQuery(); @@ -8833,6 +8834,43 @@ void TestMainWindow::aStuckComposeRequestDoesNotHijackTheNextPaneLoad() QCOMPARE(window.openComposerCount(), 0); } +void TestMainWindow::quittingClosesEveryComposerRatherThanOrphaningIt() +{ + // A composer is a parentless top-level window, deliberately: it must appear + // in the task switcher and be usable while the main window is. The cost is + // that closing the main window does NOT take it down, so quitting left a + // composer on screen with no application behind it, and Qt kept the process + // alive for it. Reported from a hand test: the main window closed, the + // orphan stayed, and its own close then raised the unsaved-edits dialog for + // a session the user had already ended. + // + // The quit path already ASKS about those edits and saves them; what it + // never did was close the windows afterwards. + WorkerComposeFixture fixture; + QVERIFY2(fixture.seed({ { QStringLiteral("work"), QStringLiteral("work"), + QString(), QStringLiteral("/bin/true"), + QStringLiteral("you@example.org") } }, + QStringLiteral("work/inbox")), + qPrintable(fixture.backed.error())); + + MainWindow window(fixture.backed.config()); + QTRY_VERIFY_WITH_TIMEOUT(!window.mailRootForTesting().isEmpty(), 15000); + + // Two, so the fix cannot be "close the last one" and pass. + QVERIFY2(window.openComposerForTest(), "no composer opened"); + QVERIFY2(window.openComposerForTest(), "no second composer opened"); + QCOMPARE(window.openComposerCount(), 2); + + // Clean composers: the point here is the CLOSE, not the unsaved-edits + // dialog, which has its own tests and would block this one on a modal. + window.show(); + window.close(); + + // deleteLater() is how a composer goes away, so the count settles on the + // next event-loop pass rather than synchronously. + QTRY_COMPARE_WITH_TIMEOUT(window.openComposerCount(), 0, 5000); +} + void TestMainWindow::theSaveLoopToleratesAComposerClosedUnderTheDialog() { // The regression for a measured use-after-free. composersBlockingQuit() |
