summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/messageview.cpp72
-rw-r--r--src/messageview.h37
2 files changed, 109 insertions, 0 deletions
diff --git a/src/messageview.cpp b/src/messageview.cpp
index d2be380..8a1976d 100644
--- a/src/messageview.cpp
+++ b/src/messageview.cpp
@@ -30,6 +30,7 @@
#include <QHBoxLayout>
#include <QLabel>
#include <QLocale>
+#include <QMenu>
#include <QMouseEvent>
#include <QPushButton>
#include <QStandardPaths>
@@ -50,6 +51,7 @@
#include "cidschemehandler.h"
#include "htmlbuilder.h"
#include "requestinterceptor.h"
+#include "searchterm.h"
#include "tagstrip.h"
#include "threadcidmap.h"
#include "version.h"
@@ -163,6 +165,12 @@ MessageView::MessageView(QWidget *parent)
m_headerLabel->setWordWrap(true);
m_headerLabel->setTextInteractionFlags(Qt::TextSelectableByMouse);
+ // Item 85: the header's values are searchable. CustomContextMenu rather
+ // than an action list, since the entries depend on what is displayed.
+ m_headerLabel->setContextMenuPolicy(Qt::CustomContextMenu);
+ connect(m_headerLabel, &QWidget::customContextMenuRequested,
+ this, &MessageView::showHeaderContextMenu);
+
// To the right of the header, per the user's decision: the summary answers
// "who is this from", this answers "what actually happened to it". A button
// and not only a shortcut, since "everything needs a memorized key" is the
@@ -428,6 +436,11 @@ QString MessageView::headerMark(Marks::Mark mark) const
void MessageView::updateHeader()
{
+ // A stale offer list must not survive either exit: the early return below
+ // leaves nothing on screen to search, and the normal path rebuilds it from
+ // scratch a few lines down.
+ m_headerOffers.clear();
+
if (m_items.isEmpty()) {
m_headerLabel->clear();
m_detailsButton->hide();
@@ -440,6 +453,30 @@ void MessageView::updateHeader()
// Re: prefixes that add nothing.
const QString subject = m_items.first().message.subject;
+ // Collected by the pass that renders the label, from the same values, so
+ // nothing has to parse the rendered markup back into structure.
+ auto elided = [](const QString &value) {
+ constexpr int kMaxLabel = 40;
+ return value.size() > kMaxLabel
+ ? value.left(kMaxLabel) + QStringLiteral("...")
+ : value;
+ };
+
+ auto offer = [this](const QString &label, const QString &query) {
+ if (query.isEmpty())
+ return;
+ m_headerOffers.append({ label, query });
+ };
+
+ offer(tr("subject \"%1\"").arg(elided(subject)),
+ SearchTerm::field(QStringLiteral("subject"), subject));
+
+ const QDateTime sent = MimeParser::parseDate(m_items.first().message.date);
+ if (sent.isValid()) {
+ offer(tr("mail from %1").arg(sent.date().toString(Qt::ISODate)),
+ SearchTerm::onDate(sent.date()));
+ }
+
// Item 70's marks, beside the subject and OUTSIDE the message area. The
// user asked for these two only: whether the thread is flagged and whether
// it carries an attachment, which are the two states worth knowing before
@@ -489,6 +526,16 @@ void MessageView::updateHeader()
row(tr("From:"), message.from);
row(tr("To:"), message.to);
row(tr("Cc:"), message.cc);
+
+ // Only here, sharing the condition with the header's own display: for
+ // a real thread these differ message to message, and the details
+ // dialog is where they are unambiguous.
+ offer(tr("sender %1").arg(elided(message.from)),
+ SearchTerm::field(QStringLiteral("from"), message.from));
+ offer(tr("recipient %1").arg(elided(message.to)),
+ SearchTerm::field(QStringLiteral("to"), message.to));
+ offer(tr("copied to %1").arg(elided(message.cc)),
+ SearchTerm::field(QStringLiteral("cc"), message.cc));
} else {
text += QStringLiteral("<br><small>%1</small>")
.arg(tr("%n message(s) in thread", "", m_items.size()));
@@ -497,6 +544,31 @@ void MessageView::updateHeader()
m_headerLabel->setText(text);
}
+void MessageView::addSearchEntries(QMenu *menu, const QList<SearchOffer> &offers)
+{
+ for (const SearchOffer &entry : offers) {
+ auto *sub = menu->addMenu(tr("Search for %1").arg(entry.label));
+
+ auto *replace = sub->addAction(tr("Search for this"));
+ connect(replace, &QAction::triggered, this,
+ [this, entry]() { emit searchRequested(entry.query, false); });
+
+ auto *narrow = sub->addAction(tr("Add to search"));
+ connect(narrow, &QAction::triggered, this,
+ [this, entry]() { emit searchRequested(entry.query, true); });
+ }
+}
+
+void MessageView::showHeaderContextMenu(const QPoint &pos)
+{
+ if (m_headerOffers.isEmpty())
+ return;
+
+ QMenu menu(this);
+ addSearchEntries(&menu, m_headerOffers);
+ menu.exec(m_headerLabel->mapToGlobal(pos));
+}
+
void MessageView::showDetailsDialog()
{
if (m_items.isEmpty())
diff --git a/src/messageview.h b/src/messageview.h
index 422869a..26ed216 100644
--- a/src/messageview.h
+++ b/src/messageview.h
@@ -25,8 +25,10 @@
#include "htmlbuilder.h"
#include "marks.h"
#include "mimeparser.h"
+#include "searchterm.h"
class QLabel;
+class QMenu;
class QPushButton;
class QWebEngineView;
class QWebEngineProfile;
@@ -127,6 +129,17 @@ public:
/// is rendered or the notice is hidden.
QString staleMessageId() const { return m_staleMessageId; }
+ /// What the header can be searched for, given what it is currently showing.
+ ///
+ /// From, To and Cc appear only for a single-message thread, which is
+ /// exactly when the header displays them: for a real thread the recipient
+ /// differs message to message and the header says only the subject and the
+ /// count. The menu must never offer a value the header is not stating.
+ ///
+ /// The values come from the same pass that renders the label, never from
+ /// parsing it back: rich text does not survive a second parse.
+ QList<SearchOffer> headerSearchOffers() const { return m_headerOffers; }
+
public slots:
void toggleHtml();
void loadRemoteContent();
@@ -158,6 +171,18 @@ signals:
void staleThreadRecoveryRequested(const QString &threadId,
const QString &messageId);
+ /// The user chose a search from one of the pane's context menus.
+ ///
+ /// `extend` narrows the current query rather than replacing it. The view
+ /// does not know what the query bar holds and must not: the window owns
+ /// that field and does the combining.
+ ///
+ /// Separate from queryRequested(), which carries a gate against a link in
+ /// a rendered document driving the thread list. These menus are chrome
+ /// built by our own code from values we extracted, so they need no gate,
+ /// and widening the existing signal would change what that gate protects.
+ void searchRequested(const QString &query, bool extend);
+
protected:
/// Turns Ctrl+wheel over the body into zoom, and Ctrl+middle-click into a
/// reset. Both events are delivered to the web view's internal QQuickWidget
@@ -207,6 +232,15 @@ private:
/// Every attachment in the thread, in the order the messages render.
QList<Attachment> allAttachments() const;
+ /// Builds and pops the header's menu at `pos`, in the label's coordinates.
+ void showHeaderContextMenu(const QPoint &pos);
+
+ /// Appends a "Search for ..." submenu per offer, each holding the replace
+ /// and the narrow operation.
+ ///
+ /// Shared with the web view's menu in a later task so the two cannot grow
+ /// different wording or a different pair of operations.
+ void addSearchEntries(QMenu *menu, const QList<SearchOffer> &offers);
QList<ThreadRenderItem> m_items;
bool m_preferHtml = true;
@@ -232,4 +266,7 @@ private:
QPushButton *m_detailsButton = nullptr;
QWidget *m_attachmentBar = nullptr;
TagStrip *m_tagStrip = nullptr;
+
+ /// Populated by updateHeader(), consumed by the header's context menu.
+ QList<SearchOffer> m_headerOffers;
};