diff options
| author | Danilo M. <danix@danix.xyz> | 2026-08-25 09:54:10 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-08-25 09:54:10 +0200 |
| commit | 01ea9e9d7df04dc771430e4c378202b8ef37b8db (patch) | |
| tree | fdca0a7328acf86f7999c89bcfd3638fe0f67ef9 /src/composewindow.h | |
| parent | 671e76f691b4027c3c9707fdae6ea6d4b4690832 (diff) | |
| download | qtmaildir-01ea9e9d7df04dc771430e4c378202b8ef37b8db.tar.gz qtmaildir-01ea9e9d7df04dc771430e4c378202b8ef37b8db.zip | |
feat(compose): report autosave state in a status bar
Autosave worked and said nothing on success. The only feedback was
m_banner, which is the failure channel and whose persistence is
load-bearing for the quit path, so success got its own channel rather
than sharing one.
The fix is a funnel, not a label. m_dirty had seven writers, four of
which clear it and only two of those are a save: the constructor clears
it because seeding is not an edit, and the send handler clears it
because the message is gone. A cue hung off saveDraftNow() would have
been silently wrong in both. setDirty() is the only writer now, and it
refreshes the status cue and setWindowModified() together so neither
display can drift from the flag.
The age line needs a tick of its own, since it moves with no edit to
drive it. Five seconds against a label that reads in tens of them.
Two defects found by probing rather than by reading. The %n plural
rendered as "2 minute(s) ago" for every English user, because Qt picks a
plural form only when a translation supplies the forms and there is no
English .ts; it uses %1 and "min" now, which Italian substitutes
identically. And the status mark was inside the translatable string,
where a translator could drop it; it is concatenated outside tr().
Presentation reworked after the user looked at it. The first version
reused item 151's yellow ribbon treatment, which reads as a misplaced
widget on a bare status label rather than as a warning, and put both
labels in the permanent widget area, which is the right-hand tray. They
are ordinary status text on the left now.
onlyTheSetterWritesTheDirtyFlag() asserts the funnel structurally, by
reading composewindow.cpp: the first test for the send path called
markClean() directly and a mutation restoring a direct assignment left
the whole suite green. Four mutations now fail. The suite still cannot
see the presentation, which is why that half needed a hand test.
lrelease reports 487 finished, 0 unfinished.
Closes item 160, and unblocks 161.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UUQS6n3cmsFrsjCNmwNtf8
Diffstat (limited to 'src/composewindow.h')
| -rw-r--r-- | src/composewindow.h | 38 |
1 files changed, 38 insertions, 0 deletions
diff --git a/src/composewindow.h b/src/composewindow.h index caff011..dc7f3c0 100644 --- a/src/composewindow.h +++ b/src/composewindow.h @@ -18,6 +18,7 @@ #pragma once +#include <QDateTime> #include <QMainWindow> #include <functional> @@ -96,6 +97,15 @@ public: /// and quitting therefore loses that text. bool lastSaveFailed() const { return m_saveFailed; } + /// Clear the unsaved-edits state without writing a draft. The send path + /// needs this: the message is gone, so there is nothing left to save and + /// nothing to warn about on the way out. + void markClean(); + + /// Render the age line as though the last save were \p seconds ago. + /// Exists so a test can drive the clock instead of waiting on it. + void reportDraftAgeFor(qint64 seconds); + /// Where the signature files live. Defaults to /// <config>/qtmaildir/signatures; a test points it at its own directory. /// @@ -217,6 +227,17 @@ private: void showSendFailure(const QString &stderrText); void applyEdit(const MarkdownFormat::Edit &edit); void markDirty(); + + /// Builds the status bar carrying the unsaved cue and the age line. + void buildDraftStatusBar(); + + /// The one writer of m_dirty. Refreshes the status cue and the title + /// marker so neither can drift from the flag. + void setDirty(bool dirty); + + /// Repaint the age line from m_lastSavedAt. Called by the tick and after + /// a save. + void refreshDraftStatus(); void autosave(); void send(); void applyFormat(const QString &token); @@ -272,6 +293,18 @@ private: /// how send_html seeds from context and is then left alone. bool m_signatureChosen = false; QLabel *m_banner = nullptr; + + /// The status bar's two halves. The cue answers "is there anything + /// unwritten"; the age answers "when did the last write happen". Both + /// are refreshed from setDirty()/refreshDraftStatus() and never assigned + /// directly, so they cannot disagree with m_dirty. + QLabel *m_unsavedCue = nullptr; + QLabel *m_draftAge = nullptr; + QTimer *m_draftAgeTick = nullptr; + + /// When the last successful save happened, invalid until one has. Drives + /// the age line, which changes with no edit to prompt it. + QDateTime m_lastSavedAt; QListWidget *m_attachmentList = nullptr; QWidget *m_sendLogPane = nullptr; QPlainTextEdit *m_sendLog = nullptr; @@ -295,6 +328,11 @@ private: /// on the bytes can never fire. It would read as working while writing a /// file, and an mbsync upload, on every debounce. QString m_savedFingerprint; + + /// Never assigned directly outside setDirty(). Seven sites used to write + /// it and four of them clear it, only two of which are a save, so a cue + /// hung off the save path alone missed the constructor and the send. The + /// setter is what keeps the flag and both displays in step. bool m_dirty = false; bool m_saveFailed = false; |
