aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/mainwindow.cpp27
-rw-r--r--tests/test_mainwindow.cpp38
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()