diff options
Diffstat (limited to 'src/mainwindow.h')
| -rw-r--r-- | src/mainwindow.h | 117 |
1 files changed, 69 insertions, 48 deletions
diff --git a/src/mainwindow.h b/src/mainwindow.h index 6321573..fa413fe 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -112,7 +112,7 @@ public: /// this is testable with no worker and no database. /// /// Scope follows the ACTION. The three queues already encode it: a held - /// thread edit carries thread ids because a `*_thread` action made it, + /// thread edit carries thread ids because a CONVERSATION row made it, /// while a netted tag edit and a held move both carry message ids. Nothing /// is expanded, and nothing is escalated. /// Net changes the index holds that a sync has not carried over. @@ -768,11 +768,23 @@ private: /// reply from reply-without-quoting, which are the same kind with and /// without a seeded body. /// - /// Resolves through ThreadListModel::messageScopeFor(), NOT threadFor(): a - /// thread row means the one message its card shows. Replying to a thread - /// is meaningless, a reply answers a message. + /// Resolved per ROW since item 177. A message row, whether a reply or a + /// thread of one, seeds the composer from that message exactly as before. + /// A CONVERSATION row has no one message to answer, so `reply` there means + /// "reply to this thread": reply-all, quoting nothing, threaded off the + /// conversation's NEWEST message so the answer lands at its end. The other + /// four compose actions are hidden on such a row rather than redefined. void composeReply(ComposeContext::Kind kind, bool quote); + /// Starts the conversation reply: asks the worker for the thread's + /// messages, and finishes in onThreadMessagesResolved(). + /// + /// A round trip because the newest message's id is not in the model. A + /// thread the user never expanded holds no nodes for its replies at all, + /// and the summary carries only the FIRST message, which is the one a + /// reply must not answer. + void replyToThread(const QString &threadId); + /// Asks the worker for \p messageId's current file, then opens a composer. /// /// The round trip is the point. The context is built from the DATABASE and @@ -835,20 +847,16 @@ private: const QString &description, const std::function<void()> &handler); - /// What a tag action acts on. + /// Applies a tag change to whatever the selection stands for. /// - /// Since item 108 a thread ROW means the one message its card displays, so - /// Message is the default and Thread is the explicit choice the user makes - /// through the "Whole thread" submenu. Before that there was no choice: - /// a thread row always meant the conversation. - enum class TagScope { - Message, ///< The message each selected row displays. - Thread, ///< Every message of each selected row's thread. - }; - + /// There is no scope parameter, and that is item 177: the ROW decides. + /// A conversation row means its conversation, any other row means its one + /// message, and ThreadListModel::scopeForSelection() is the only place + /// that answer is worked out. A caller that could choose is how the same + /// gesture came to mean two things, which is what the deleted "Whole + /// thread" submenu existed to disambiguate. void tagSelected(const QStringList &add, const QStringList &remove, - const QString &description, - TagScope scope = TagScope::Message); + const QString &description); /// Starts, restarts or cancels the mark-read timer for a newly opened /// MESSAGE. Cancels outright for one that is not unread, so an already read @@ -921,32 +929,18 @@ private: void updateSyncControls(); - /// Opens the tag dialog on the current selection and applies its result. - /// - /// The only route to an arbitrary tag: every other tag action writes a - /// hardcoded name. - /// The "Whole thread" submenu, built fresh for each parent that needs one. - /// - /// A QMenu lives in one menu tree, so the menu bar and the context menu get - /// their own instance. The actions inside are shared, which is what has to - /// stay consistent between them. - QMenu *buildThreadActionsMenu(QWidget *parent); - /// Per-tag counts across the selected rows, for the tag dialog. QHash<QString, int> selectionTagCounts() const; /// True when every selected row already carries \p tag, which is what a /// toggle asks before choosing its direction. /// - /// Under Message scope each row answers about what it STANDS FOR: a reply - /// row about its message, a thread row about the message its card - /// displays. Asking a reply's thread makes a toggle one-way, since the - /// message-scoped write never changes the thread's tags. - /// - /// Under Thread scope a row answers about its whole thread, so the - /// question matches the write the thread actions are about to make. - bool everySelectedRowHasTag(const QString &tag, - TagScope scope = TagScope::Message) const; + /// Each row answers about what it STANDS FOR, which is the same question + /// the write asks: a conversation row about its whole thread, any other + /// row about its one message. Asking a reply's thread makes a toggle + /// one-way, since the message-scoped write never changes the thread's + /// tags and the answer therefore never moves. + bool everySelectedRowHasTag(const QString &tag) const; /// The three-valued version of the question above, which is what a LABEL /// needs and a toggle's direction does not. @@ -958,13 +952,37 @@ private: /// is what happens when a two-valued predicate is asked a three-valued /// question. enum class TagPresence { None, Every, Mixed }; - TagPresence selectionTagPresence( - const QString &tag, TagScope scope = TagScope::Message) const; + TagPresence selectionTagPresence(const QString &tag) const; /// Relabels the unread action, and hides it when the selection has no /// single state. Called whenever the selection changes. void refreshUnreadAction(); + /// Makes every scoped action say what it will act on, and hides the ones + /// that cannot mean anything on the selected row (item 177). + /// + /// Two separate jobs, deliberately in one pass over one selection so the + /// label and the visibility cannot disagree. Delete and Archive name the + /// thread on a conversation row and are ABSENT on a reply, per the user: + /// a single reply cannot be removed from a conversation. Forward and Save + /// are the mirror, absent on a conversation row, which shows no message + /// to forward and names no file to write. + void refreshScopedActionLabels(); + + /// Whether every selected row is a conversation, a message, or neither + /// because the selection mixes them or is empty. + enum class SelectionKind { Empty, Conversations, Messages, Mixed }; + SelectionKind selectionKind() const; + + /// Whether the selection holds a reply row, which is what hides Delete, + /// Restore and Archive (item 177). + /// + /// Written by refreshScopedActionLabels() and read by + /// refreshTrashActions(), which runs after it and owns the same two + /// actions' visibility. A flag rather than a second walk over the + /// selection, so the two cannot answer differently. + bool m_replySelectionHidesDelete = false; + /// Hides Delete on mail already in the trash, and Restore on mail that /// was never there (item 168). Each is offered only where it means /// something, the same rule refreshUnreadAction() applies to the label. @@ -979,6 +997,10 @@ private: /// Delete on exactly the mail a trash view is full of. bool everySelectedRowIsInATrashFolder() const; + /// Opens the tag dialog on the current selection and applies its result. + /// + /// The only route to an arbitrary tag: every other tag action writes a + /// hardcoded name. void editTagsOnSelection(); /// Set once the user has answered the exit prompt, or once a sync started @@ -1052,21 +1074,20 @@ private: int messageCount, const QStringList &wholeThreadIds = {}); - /// Moves every message of each selected THREAD to its account's trash. + /// Moves every message of the named THREADS to their accounts' trash. /// /// Asynchronous, unlike its message-scoped twin: the ids and paths of an /// unexpanded thread's messages live only in the database, so this asks /// the worker and finishes in onThreadMessagesResolved(). - void trashSelectedThreads(); - - /// The thread ids the selection covers, resolving a reply row to its own - /// thread. scopeFor() reports a reply under messageIds instead, which left - /// a thread action on a reply row doing nothing at all. - QStringList selectedThreadIds() const; + /// + /// Takes ids rather than reading the selection, because since item 177 the + /// conversation half of a delete is one branch of trashSelected() rather + /// than a separate action the user could pick. + void trashThreads(const QStringList &threadIds); - /// The inverse of trashSelectedThreads(): moves every message of each - /// selected thread back where it came from. - void restoreSelectedThreads(); + /// The inverse of trashThreads(): moves every message of the named threads + /// back where it came from. + void untrashThreads(const QStringList &threadIds); /// Runs the thread-scoped delete once the worker has resolved the /// threads to messages. |
