diff options
| -rw-r--r-- | src/mainwindow.cpp | 11 | ||||
| -rw-r--r-- | src/mainwindow.h | 8 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 121 |
3 files changed, 140 insertions, 0 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index cb02652..11de1d4 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -585,6 +585,17 @@ void MainWindow::buildUi() m_splitter->addWidget(m_threadView); m_splitter->addWidget(m_messageView); m_splitter->setStretchFactor(1, 2); + // A splitter position is saved in PIXELS, so one saved in a wide window + // does not fit a narrower one: QSplitter restores the first pane's size + // verbatim and gives the second whatever is left. A real 1285/1252 split + // restored into a 1136px window left the message pane 29px wide, a sliver + // of rendered mail beside a full-width thread list. A floor on the pane + // covers that and the equivalent drag, and needs no restore-time repair. + // Only the message pane: a minimum on the thread view as well would leave + // a narrow window unable to satisfy either, the same fault from the other + // side. + m_splitter->setCollapsible(1, false); + m_messageView->setMinimumWidth(kMinMessagePaneWidth); layout->addWidget(m_splitter, 1); layout->addWidget(m_syncLogPane); diff --git a/src/mainwindow.h b/src/mainwindow.h index 490467f..d8401a3 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -211,6 +211,7 @@ private: /// Restores window geometry, splitter and thread-list header widths. /// A missing or rejected blob leaves the buildUi() defaults in place. void restoreUiState(); + void saveUiState() const; void registerActions(); @@ -415,6 +416,13 @@ private: /// menu bar does. QMenu *m_threadContextMenu = nullptr; QSplitter *m_splitter = nullptr; + + /// Below this the message pane shows a sliver of a rendered mail and is + /// not worth the space it occupies. This is a floor, reached only when a + /// restored position does not fit, so it is set at the width mail is + /// readable at rather than the width it is merely visible at: at 200 the + /// placeholder's own text wraps every couple of words. + static constexpr int kMinMessagePaneWidth = 300; QComboBox *m_accountBox = nullptr; QLabel *m_statusLabel = nullptr; diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index fc7409c..5031ead 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -35,6 +35,7 @@ #include <QTemporaryDir> #include <QStyle> +#include <QSplitter> #include <QTableView> #include <QToolBar> #include <QTimer> @@ -64,6 +65,8 @@ private slots: void uiStateIsNotWrittenIntoTheUserConfig(); void uiStateSurvivesARestart(); void missingUiStateLeavesTheDefaults(); + void splitterStateFromAWiderWindowKeepsTheMessagePane(); + void usableSplitterStateIsRestoredUntouched(); void headerStateFromADifferentColumnLayoutIsDiscarded(); void returnInTheQueryBarRunsTheQueryNotOpenThread(); void markReadTimerRestartsRatherThanStacking(); @@ -301,6 +304,124 @@ void TestMainWindow::missingUiStateLeavesTheDefaults() QStandardPaths::setTestModeEnabled(false); } +void TestMainWindow::splitterStateFromAWiderWindowKeepsTheMessagePane() +{ + // The reported symptom, from a real state file: a splitter position saved + // at 1285/1252 and restored into a 1136px window. QSplitter honours the + // first size and gives the second what is left, so the message pane came + // back roughly 30px wide, a sliver of rendered mail against a full-width + // thread list. It is not a first-run problem, which is why widening the + // default window would not have helped: the wider the window ever was, the + // worse the next narrower session is. + QStandardPaths::setTestModeEnabled(true); + QFile::remove(MainWindow::uiStatePath()); + + { + const Config config; + MainWindow window(config); + window.show(); + QVERIFY(QTest::qWaitForWindowExposed(&window)); + + auto *splitter = window.findChild<QSplitter *>(); + QVERIFY(splitter); + // Guard: the forged sizes have to be genuinely wider than the window + // this reopens into, or the test proves nothing. + QVERIFY(splitter->width() > 0); + // Times four, not times two: the offscreen platform chooses the window + // width itself and has been seen to choose differently between runs of + // this binary, so a margin that only just exceeds THIS window's width + // can fail to exceed the reopened one's and quietly stop reproducing + // the bug. + const int overwide = splitter->width() * 4; + splitter->setSizes({ overwide, overwide }); + window.close(); + } + + const Config config; + MainWindow reopened(config); + reopened.show(); + QVERIFY(QTest::qWaitForWindowExposed(&reopened)); + + auto *splitter = reopened.findChild<QSplitter *>(); + QVERIFY(splitter); + const QList<int> sizes = splitter->sizes(); + QCOMPARE(sizes.size(), 2); + + // Asserted against the pane's OWN minimum rather than a number repeated + // here: the floor is private to MainWindow, and a literal copy would keep + // passing if the shipped one were lowered back to a width mail cannot be + // read at. The guard below is what stops that from being vacuous. + auto *pane = reopened.findChild<MessageView *>(); + QVERIFY(pane); + QVERIFY2(pane->minimumWidth() >= 300, + qPrintable(QStringLiteral("message pane floor is %1px") + .arg(pane->minimumWidth()))); + QVERIFY2(sizes.at(1) >= pane->minimumWidth(), + qPrintable(QStringLiteral("message pane restored %1px wide in a " + "%2px splitter") + .arg(sizes.at(1)) + .arg(splitter->width()))); + + reopened.close(); + QFile::remove(MainWindow::uiStatePath()); + QStandardPaths::setTestModeEnabled(false); +} + +void TestMainWindow::usableSplitterStateIsRestoredUntouched() +{ + // The rescue must not fire on a position that fits. This is item 1's whole + // point: a splitter the user dragged comes back where they left it. + QStandardPaths::setTestModeEnabled(true); + QFile::remove(MainWindow::uiStatePath()); + + QList<int> saved; + { + const Config config; + MainWindow window(config); + window.show(); + QVERIFY(QTest::qWaitForWindowExposed(&window)); + + auto *splitter = window.findChild<QSplitter *>(); + QVERIFY(splitter); + // Lopsided but both panes usable, so nothing should touch it. Sized as + // a fraction rather than "width minus 250": this window's width is the + // platform's to choose and has been seen to differ between two runs of + // the same binary, so an absolute split saved here can arrive too wide + // for the reopened window and trip the rescue the test is asserting + // does NOT fire. + const int total = splitter->width(); + QVERIFY(total > 500); + splitter->setSizes({ total / 4, total - total / 4 }); + saved = splitter->sizes(); + QVERIFY(saved.at(0) < saved.at(1)); + window.close(); + } + + const Config config; + MainWindow reopened(config); + reopened.show(); + QVERIFY(QTest::qWaitForWindowExposed(&reopened)); + + auto *splitter = reopened.findChild<QSplitter *>(); + QVERIFY(splitter); + // The saved ratio, not the saved pixels: QSplitter redistributes to the + // current width, and only a rescue would flip which pane is the larger. + // The FIRST pane's saved pixel width is what must survive: QSplitter + // restores it verbatim and gives the second whatever the current window + // leaves, so this is the value a rescue would overwrite and the only one + // that does not move with the window width. Which matters here, because + // the offscreen platform picks that width itself and has been seen to + // pick differently between two runs of this same binary (1181 and 779), + // so any assertion on a ratio or on pane 1 is checking the platform's + // mood rather than this code. + const QList<int> sizes = splitter->sizes(); + QCOMPARE(sizes.at(0), saved.at(0)); + + reopened.close(); + QFile::remove(MainWindow::uiStatePath()); + QStandardPaths::setTestModeEnabled(false); +} + void TestMainWindow::headerStateFromADifferentColumnLayoutIsDiscarded() { // The upgrade hazard: a 0.3.0 state file holds a three-column header blob, |
