aboutsummaryrefslogtreecommitdiffstats
path: root/src/m_email/EMailPageView.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/m_email/EMailPageView.cpp')
-rw-r--r--src/m_email/EMailPageView.cpp731
1 files changed, 422 insertions, 309 deletions
diff --git a/src/m_email/EMailPageView.cpp b/src/m_email/EMailPageView.cpp
index 8351b01..4c7e8f7 100644
--- a/src/m_email/EMailPageView.cpp
+++ b/src/m_email/EMailPageView.cpp
@@ -60,6 +60,7 @@
#include <QMenu>
#include <QMessageBox>
#include <QMimeData>
+#include <QPainter>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QSaveFile>
@@ -111,6 +112,35 @@ constexpr int kPersistDelayMs = 500;
/// below it the list is smaller than its own header plus a row.
constexpr int kMinAttachmentHeight = 120;
+/// A chevron in the palette's button text colour: the same Expand / Collapse
+/// mark as the host's collapsed Status Panel, so the two compact rows read as
+/// one pattern. Drawn rather than taken from the style, whose arrows are
+/// coloured and differ between platforms.
+auto EMailChevronIcon(const QPalette& palette, bool up) -> QIcon {
+ constexpr int kSize = 16;
+ const qreal dpr = qApp != nullptr ? qApp->devicePixelRatio() : 1.0;
+ QPixmap pixmap(QSize(kSize, kSize) * dpr);
+ pixmap.setDevicePixelRatio(dpr);
+ pixmap.fill(Qt::transparent);
+
+ QPainter painter(&pixmap);
+ painter.setRenderHint(QPainter::Antialiasing);
+ QPen pen(palette.color(QPalette::ButtonText), 1.6);
+ pen.setCapStyle(Qt::RoundCap);
+ pen.setJoinStyle(Qt::RoundJoin);
+ painter.setPen(pen);
+
+ const qreal mid = kSize / 2.0;
+ const qreal half = 4.0;
+ const qreal rise = up ? -2.0 : 2.0;
+ const std::array<QPointF, 3> points = {QPointF(mid - half, mid - rise),
+ QPointF(mid, mid + rise),
+ QPointF(mid + half, mid - rise)};
+ painter.drawPolyline(points.data(), static_cast<int>(points.size()));
+ painter.end();
+ return QIcon(pixmap);
+}
+
/// Names the completer so it can be found again on the line edit it belongs
/// to. Needed because a multi-value completer is not reachable through
/// QLineEdit::completer(): it is driven by hand and only parented to the edit.
@@ -475,7 +505,6 @@ void EMailPageView::apply_colors() {
}
if (envelope_rule_ != nullptr) EMailPaintRule(envelope_rule_);
- if (action_separator_ != nullptr) EMailPaintRule(action_separator_);
if (locked_banner_ != nullptr) {
EMailTintBanner(locked_banner_, MutedColor(this), kLockedTint);
@@ -489,6 +518,7 @@ void EMailPageView::apply_colors() {
if (attachment_heading_ != nullptr) {
EMailSetLabelColor(attachment_heading_, MutedColor(this));
}
+ if (attachment_toggle_ != nullptr) apply_attachment_bar();
if (unsigned_notice_ != nullptr) {
EMailSetLabelColor(unsigned_notice_, ThemeColor(this, GF_UI_COLOR_WARNING));
}
@@ -685,110 +715,160 @@ auto EMailPageView::build_message_surface() -> QWidget* {
body_row->addWidget(view_switcher_);
body_row->addSpacing(4);
- // Message-level actions. Each produces a NEW document in a new tab; none of
- // them writes to this one.
+ // Every command is a QAction owned by the view and registered on it, so its
+ // shortcut works wherever the command is currently shown -- on the row, or
+ // in a menu. Buttons and menus show the same actions; nothing is a copy.
//
- // Icons come from the desktop theme where there is one, because these three
+ // Icons come from the desktop theme where there is one, because these
// actions have long-settled, instantly recognisable icons that no bundled
// substitute would improve on. The bundled files are the fallback for the
// platforms that have no icon theme at all.
- const auto make_action =
- [this, body_row](const QString& theme_icon, const QString& fallback_icon,
- const QString& text, const QString& tip,
- const QKeySequence& shortcut) {
- auto* button = new QToolButton(this);
- button->setIcon(QIcon::fromTheme(theme_icon, QIcon(fallback_icon)));
- button->setText(text);
- button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- button->setAutoRaise(true);
- button->setVisible(false);
-
- // The key and the sentence advertising it are set together, so they
- // cannot drift apart. A shortcut nothing mentions is a shortcut only
- // the person who wrote it knows about.
- button->setShortcut(shortcut);
- button->setToolTip(
- shortcut.isEmpty()
- ? tip
- : tr("%1 (%2)").arg(
- tip, shortcut.toString(QKeySequence::NativeText)));
-
- body_row->addWidget(button);
- return button;
- };
+ const auto make_command = [this](const QString& theme_icon,
+ const QString& fallback_icon,
+ const QString& text, const QString& tip,
+ const QKeySequence& shortcut) {
+ auto* action = new QAction(
+ QIcon::fromTheme(theme_icon, QIcon(fallback_icon)), text, this);
+ // The key and the sentence advertising it are set together, so they
+ // cannot drift apart. A shortcut nothing mentions is a shortcut only the
+ // person who wrote it knows about.
+ action->setShortcut(shortcut);
+ action->setShortcutContext(Qt::WidgetWithChildrenShortcut);
+ action->setToolTip(
+ shortcut.isEmpty()
+ ? tip
+ : tr("%1 (%2)").arg(tip,
+ shortcut.toString(QKeySequence::NativeText)));
+ addAction(action);
+ return action;
+ };
+ const auto make_button = [this, body_row](QAction* action) {
+ auto* button = new QToolButton(this);
+ button->setDefaultAction(action);
+ button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ button->setAutoRaise(true);
+ button->setVisible(false);
+ body_row->addWidget(button);
+ return button;
+ };
- reply_button_ = make_action(
+ // Message-level actions. Each produces a NEW document in a new tab; none of
+ // them writes to this one.
+ reply_act_ = make_command(
QStringLiteral("mail-reply-sender"), ":/icons/reply.png", tr("Reply"),
tr("Write a reply to the sender."), QKeySequence(Qt::CTRL | Qt::Key_R));
- reply_all_button_ = make_action(
+ reply_all_act_ = make_command(
QStringLiteral("mail-reply-all"), ":/icons/reply-all.png",
tr("Reply All"),
tr("Write a reply to the sender and everyone else who was addressed. "
"Blind recipients are not included."),
QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_R));
- forward_button_ = make_action(
+ forward_act_ = make_command(
QStringLiteral("mail-forward"), ":/icons/redo.png", tr("Forward"),
tr("Pass this message on, with its attachments."),
QKeySequence(Qt::CTRL | Qt::Key_L));
+ send_act_ = make_command(QStringLiteral("mail-send"),
+ ":/icons/export-email.png", tr("Send..."),
+ tr("Send this message through a configured "
+ "mail account."),
+ QKeySequence(Qt::CTRL | Qt::Key_Return));
+ // The attachment menu's Attach is this same action, so the row and the
+ // menu cannot disagree about it.
+ attachment_attach_act_ = make_command(
+ QStringLiteral("mail-attachment"), ":/icons/attachment.png",
+ tr("Attach File..."),
+ tr("Add one or more files to this message. Files can also be dropped "
+ "onto the message."),
+ QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A));
+ details_act_ = make_command(
+ QStringLiteral("dialog-information"), ":/icons/detail.png",
+ // The ellipsis is doing its usual job: this opens a window rather than
+ // changing anything here.
+ tr("Details..."),
+ tr("Show what is known about this message: its signatures, its "
+ "structure and its headers."),
+ QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_D));
+ forensic_act_ = make_command(
+ QStringLiteral("object-locked"), ":/icons/read-only.png", tr("Read-only"),
+ tr("Lock this message so it cannot be edited or rewritten. Reply and "
+ "Forward still work and produce new messages."),
+ QKeySequence());
+ forensic_act_->setCheckable(true);
- connect(reply_button_, &QToolButton::clicked, this, [this]() {
+ connect(reply_act_, &QAction::triggered, this, [this]() {
slot_derive_message(static_cast<int>(EMailReplyMode::kREPLY));
});
- connect(reply_all_button_, &QToolButton::clicked, this, [this]() {
+ connect(reply_all_act_, &QAction::triggered, this, [this]() {
slot_derive_message(static_cast<int>(EMailReplyMode::kREPLY_ALL));
});
- connect(forward_button_, &QToolButton::clicked, this, [this]() {
+ connect(forward_act_, &QAction::triggered, this, [this]() {
slot_derive_message(static_cast<int>(EMailReplyMode::kFORWARD));
});
-
- send_button_ = make_action(QStringLiteral("mail-send"),
- ":/icons/export-email.png", tr("Send..."),
- tr("Send this message through a configured "
- "mail account."),
- QKeySequence(Qt::CTRL | Qt::Key_Return));
- connect(send_button_, &QToolButton::clicked, this,
+ connect(send_act_, &QAction::triggered, this,
&EMailPageView::slot_send_message);
+ connect(details_act_, &QAction::triggered, this,
+ [this]() { open_details(); });
+ connect(forensic_act_, &QAction::toggled, this,
+ [this](bool on) { SetForensicMode(on); });
- // Locking the document is a different kind of act from deriving a new
- // message out of it, so it is set apart rather than lined up with them.
- auto* action_separator = EMailRule(this, Qt::Vertical);
- action_separator->setVisible(false);
- body_row->addSpacing(4);
- body_row->addWidget(action_separator);
- body_row->addSpacing(4);
- action_separator_ = action_separator;
+ // On the row: what this kind of document is for. A received message is
+ // answered, a draft is sent; see EMailActionBar().
+ reply_button_ = make_button(reply_act_);
+ reply_all_button_ = make_button(reply_all_act_);
+ forward_button_ = make_button(forward_act_);
+ send_button_ = make_button(send_act_);
+ add_button_ = make_button(attachment_attach_act_);
- forensic_toggle_ = make_action(
- QStringLiteral("object-locked"), ":/icons/read-only.png", tr("Read-only"),
- tr("Lock this message so it cannot be edited or rewritten. Reply and "
- "Forward still work and produce new messages."),
- QKeySequence());
- forensic_toggle_->setCheckable(true);
- connect(forensic_toggle_, &QToolButton::toggled, this,
- [this](bool on) { SetForensicMode(on); });
+ // The attachment operations, in one place, and only while there are
+ // attachments to operate on. Built here, filled in with the section below.
+ attachment_menu_ = new QMenu(this);
+ attachments_button_ = new QToolButton(this);
+ attachments_button_->setIcon(QIcon::fromTheme(
+ QStringLiteral("mail-attachment"), QIcon(":/icons/attachment.png")));
+ attachments_button_->setText(tr("Attachments"));
+ attachments_button_->setToolTip(tr("Open, save and manage attachments"));
+ attachments_button_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ attachments_button_->setAutoRaise(true);
+ attachments_button_->setPopupMode(QToolButton::InstantPopup);
+ attachments_button_->setMenu(attachment_menu_);
+ attachments_button_->setVisible(false);
+ body_row->addWidget(attachments_button_);
body_row->addStretch();
- // Opens the pane beside the message. Next to the security button because
- // the two are the same subject: what this message is, and the detail behind
- // that answer.
- details_button_ = new QToolButton(this);
- details_button_->setIcon(QIcon::fromTheme(
- QStringLiteral("dialog-information"), QIcon(":/icons/detail.png")));
- // The ellipsis is doing its usual job: this opens a window rather than
- // changing anything here.
- details_button_->setText(tr("Details..."));
- details_button_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- details_button_->setAutoRaise(true);
- details_button_->setShortcut(QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_D));
- details_button_->setToolTip(tr("%1 (%2)").arg(
- tr("Show what is known about this message: its signatures, its "
- "structure and its headers."),
- details_button_->shortcut().toString(QKeySequence::NativeText)));
- details_button_->setMinimumWidth(details_button_->sizeHint().width());
- connect(details_button_, &QToolButton::clicked, this,
- [this]() { open_details(); });
- body_row->addWidget(details_button_);
+ // Everything reachable that is not done with every message.
+ more_menu_ = new QMenu(this);
+ more_menu_->setToolTipsVisible(true);
+ more_answer_separator_ = more_menu_->addSeparator();
+ more_menu_->addAction(details_act_);
+ more_lock_separator_ = more_menu_->addSeparator();
+ more_menu_->addAction(forensic_act_);
+
+ more_button_ = new QToolButton(this);
+ more_button_->setText(tr("More"));
+ more_button_->setToolTip(tr("More actions for this message"));
+ more_button_->setIcon(QIcon::fromTheme(QStringLiteral("view-more"),
+ QIcon(":/icons/detail.png")));
+ more_button_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ more_button_->setAutoRaise(true);
+ more_button_->setPopupMode(QToolButton::InstantPopup);
+ more_button_->setMenu(more_menu_);
+ body_row->addWidget(more_button_);
+ body_row->addSpacing(4);
+
+ // A state, not a control: the lock itself is toggled from More.
+ read_only_label_ = new QLabel(this);
+ read_only_label_->setObjectName(QStringLiteral("emailReadOnlyStatus"));
+ read_only_label_->setText(tr("Read-only"));
+ read_only_label_->setToolTip(
+ tr("This message is open for inspection only. Turn it off in More."));
+ {
+ auto font = read_only_label_->font();
+ font.setPointSizeF(font.pointSizeF() * 0.92);
+ read_only_label_->setFont(font);
+ }
+ read_only_label_->setVisible(false);
+ body_row->addWidget(read_only_label_);
body_row->addSpacing(4);
// What the message IS, and which action applies, on the same row as the
@@ -820,6 +900,10 @@ auto EMailPageView::build_message_surface() -> QWidget* {
layout->addLayout(body_row);
body_edit_ = new QPlainTextEdit(this);
+ // It wraps at the viewport's width: a scrollbar appearing because the
+ // window's Status Panel expanded would reflow the message sideways, so the
+ // gutter is kept whether or not there is anything to scroll.
+ body_edit_->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOn);
body_edit_->setPlaceholderText(tr("Write your message here."));
body_view_ = new EMailBodyView(this);
@@ -856,35 +940,54 @@ auto EMailPageView::build_message_surface() -> QWidget* {
// Colour belongs to apply_colors(), like every other colour in this view.
banner_row->addWidget(locked_notice_, 1);
- layout->addWidget(locked_banner_);
-
locked_panel_ = build_locked_panel();
body_stack_ = new QStackedWidget(this);
body_stack_->addWidget(body_edit_);
body_stack_->addWidget(body_view_);
body_stack_->addWidget(locked_panel_);
- layout->addWidget(body_stack_, 1);
// Everything about attachments, wrapped for the same reason as the envelope:
// the raw source mode hides it whole.
attachment_box_ = new QWidget(page);
auto* attachment_column = new QVBoxLayout(attachment_box_);
- attachment_column->setContentsMargins(0, 0, 0, 0);
- attachment_column->setSpacing(6);
- layout->addWidget(attachment_box_);
+ attachment_column->setContentsMargins(0, 2, 0, 0);
+ attachment_column->setSpacing(4);
- attachment_heading_ = new QLabel(this);
- {
- auto font = attachment_heading_->font();
+ // One row, the shape of the collapsed Status Panel: what it is, a summary,
+ // then only the actions that fit the mode, a menu for the rest, and Expand.
+ // The full table only exists on request, so a message with one part costs
+ // one line of height rather than a header, a row and a button strip.
+ auto* bar = new QHBoxLayout();
+ bar->setContentsMargins(0, 0, 0, 0);
+ bar->setSpacing(4);
+
+ const auto small_font = [this]() {
+ auto font = this->font();
font.setPointSizeF(font.pointSizeF() * 0.92);
- attachment_heading_->setFont(font);
+ return font;
+ };
+
+ attachment_icon_ = new QLabel(this);
+ attachment_icon_->setPixmap(
+ QIcon::fromTheme(QStringLiteral("mail-attachment"),
+ QIcon(":/icons/attachment.png"))
+ .pixmap(16, 16));
+
+ attachment_title_ = new QLabel(tr("Attachments"), this);
+ {
+ auto font = small_font();
+ font.setWeight(QFont::DemiBold);
+ attachment_title_->setFont(font);
}
- attachment_heading_->setVisible(false);
- // Not welded to the body above it: this is a new group, not a caption on the
- // editor.
- attachment_column->addSpacing(2);
- attachment_column->addWidget(attachment_heading_);
+
+ // The summary: the one part's name and size, or how many there are. Allowed
+ // to shrink, so a long file name gives way before the actions do.
+ attachment_heading_ = new QLabel(this);
+ attachment_heading_->setFont(small_font());
+ attachment_heading_->setSizePolicy(QSizePolicy::Ignored,
+ QSizePolicy::Preferred);
+ attachment_heading_->setTextFormat(Qt::PlainText);
attachment_list_ = new QTreeWidget(this);
attachment_list_->setRootIsDecorated(false);
@@ -898,84 +1001,93 @@ auto EMailPageView::build_message_surface() -> QWidget* {
QHeaderView::Stretch);
EMailPolishTree(attachment_list_);
attachment_list_->setVisible(false);
- attachment_column->addWidget(attachment_list_);
unsigned_notice_ = new QLabel(this);
unsigned_notice_->setWordWrap(true);
unsigned_notice_->setVisible(false);
- {
- auto font = unsigned_notice_->font();
- font.setPointSizeF(font.pointSizeF() * 0.92);
- unsigned_notice_->setFont(font);
- }
- attachment_column->addWidget(unsigned_notice_);
-
- // The same shape as the actions above: flat, icon beside text. These used
- // to be raised push buttons, which gave the attachment list a heavier
- // footer than the message itself.
- auto* buttons = new QHBoxLayout();
- buttons->setContentsMargins(0, 0, 0, 0);
- buttons->setSpacing(2);
-
- const auto make_attachment_action =
- [this](const QString& theme_icon, const QString& fallback_icon,
- const QString& text, const QString& tip,
- const QKeySequence& shortcut = {}) {
- auto* button = new QToolButton(this);
- button->setIcon(QIcon::fromTheme(theme_icon, QIcon(fallback_icon)));
- button->setText(text);
- button->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
- button->setAutoRaise(true);
+ unsigned_notice_->setFont(small_font());
- button->setShortcut(shortcut);
- button->setToolTip(
- shortcut.isEmpty()
- ? tip
- : tr("%1 (%2)").arg(
- tip, shortcut.toString(QKeySequence::NativeText)));
- return button;
- };
-
- add_button_ = make_attachment_action(
- QStringLiteral("mail-attachment"), ":/icons/attachment.png",
- tr("Attach File..."),
- tr("Add one or more files to this message. Files can also be dropped "
- "onto the message."),
- QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A));
+ // The operations live in the action row's Attachments menu (built above),
+ // so this bar is information only and never repeats them. Open acts on one
+ // part, Save on the parts the user can see chosen, Save All on every one.
+ attachment_open_act_ = attachment_menu_->addAction(tr("Open"));
+ attachment_save_act_ = attachment_menu_->addAction(
+ QIcon::fromTheme(QStringLiteral("document-save"),
+ QIcon(":/icons/filesave.png")),
+ tr("Save..."));
+ attachment_save_act_->setToolTip(
+ tr("Write the selected attachments to a folder."));
+ attachment_save_all_act_ = attachment_menu_->addAction(
+ QIcon::fromTheme(QStringLiteral("document-save-all"),
+ QIcon(":/icons/save-all.png")),
+ tr("Save All..."));
+ attachment_save_all_act_->setToolTip(tr("Write every attachment to a folder."));
+ attachment_edit_separator_ = attachment_menu_->addSeparator();
+ attachment_menu_->addAction(attachment_attach_act_);
// Removing takes a part out of the message being composed; it deletes
// nothing on disk, and the wording and icon both stay away from suggesting
// it does.
- remove_button_ = make_attachment_action(
- QStringLiteral("list-remove"), ":/icons/remove.png", tr("Remove"),
+ attachment_remove_act_ = attachment_menu_->addAction(
+ QIcon::fromTheme(QStringLiteral("list-remove"),
+ QIcon(":/icons/remove.png")),
+ tr("Remove"));
+ attachment_remove_act_->setToolTip(
tr("Take the selected attachments out of this message."));
- save_button_ = make_attachment_action(
- QStringLiteral("document-save"), ":/icons/filesave.png", tr("Save..."),
- tr("Write the selected attachments to a folder."));
- save_all_button_ = make_attachment_action(
- QStringLiteral("document-save-all"), ":/icons/save-all.png",
- tr("Save All..."), tr("Write every attachment to a folder."));
+ attachment_menu_->setToolTipsVisible(true);
- buttons->addWidget(add_button_);
- buttons->addWidget(remove_button_);
- buttons->addStretch();
- buttons->addWidget(save_button_);
- buttons->addWidget(save_all_button_);
- attachment_column->addLayout(buttons);
+ attachment_toggle_ = new QToolButton(this);
+ attachment_toggle_->setAutoRaise(true);
+ attachment_toggle_->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ connect(attachment_toggle_, &QToolButton::clicked, this, [this]() {
+ attachments_expanded_ = !attachments_expanded_;
+ apply_attachment_bar();
+ });
- connect(add_button_, &QToolButton::clicked, this,
- &EMailPageView::slot_add_attachment);
- connect(remove_button_, &QToolButton::clicked, this,
- &EMailPageView::slot_remove_attachment);
- connect(save_button_, &QToolButton::clicked, this,
+ bar->addWidget(attachment_icon_);
+ bar->addWidget(attachment_title_);
+ bar->addSpacing(4);
+ bar->addWidget(attachment_heading_, 1);
+ bar->addWidget(attachment_toggle_);
+ attachment_column->addLayout(bar);
+ attachment_column->addWidget(attachment_list_);
+ attachment_column->addWidget(unsigned_notice_);
+
+ connect(attachment_open_act_, &QAction::triggered, this, [this]() {
+ const auto rows = attachment_target_rows();
+ if (rows.size() == 1) {
+ open_attachment(attachment_list_->topLevelItem(rows.first()));
+ }
+ });
+ connect(attachment_save_act_, &QAction::triggered, this,
&EMailPageView::slot_save_attachment);
- connect(save_all_button_, &QToolButton::clicked, this,
+ connect(attachment_save_all_act_, &QAction::triggered, this,
&EMailPageView::slot_save_all_attachments);
+ connect(attachment_attach_act_, &QAction::triggered, this,
+ &EMailPageView::slot_add_attachment);
+ connect(attachment_remove_act_, &QAction::triggered, this,
+ &EMailPageView::slot_remove_attachment);
+
+ // Body, attachments, then the notice: see kEMailSurfaceOrder.
+ for (const auto part : kEMailSurfaceOrder) {
+ switch (part) {
+ case EMailSurfacePart::kBODY:
+ layout->addWidget(body_stack_, 1);
+ break;
+ case EMailSurfacePart::kATTACHMENTS:
+ layout->addWidget(attachment_box_);
+ break;
+ case EMailSurfacePart::kNOTICE:
+ layout->addWidget(locked_banner_);
+ break;
+ }
+ }
+
connect(attachment_list_, &QTreeWidget::itemSelectionChanged, this,
&EMailPageView::slot_selection_changed);
- // Everything the four buttons below do, on the row the user is pointing at.
- // The buttons stay: this is the second way to reach them, for the people who
- // look for it on the thing itself.
+ // Everything the row's actions do, on the row the user is pointing at: the
+ // second way to reach them, for the people who look for it on the thing
+ // itself.
attachment_list_->setContextMenuPolicy(Qt::CustomContextMenu);
connect(attachment_list_, &QTreeWidget::customContextMenuRequested, this,
&EMailPageView::slot_attachment_menu);
@@ -1092,7 +1204,7 @@ void EMailPageView::set_source_mode(bool on) {
// looking at, so they go away and the document gets the whole area.
envelope_box_->setVisible(!on);
envelope_rule_->setVisible(!on);
- attachment_box_->setVisible(!on);
+ apply_attachment_bar();
if (on) {
body_stack_->setCurrentWidget(raw_tab_);
@@ -1116,7 +1228,8 @@ void EMailPageView::refresh_action_density() {
const auto style =
compact ? Qt::ToolButtonIconOnly : Qt::ToolButtonTextBesideIcon;
for (auto* button : {reply_button_, reply_all_button_, forward_button_,
- send_button_, forensic_toggle_}) {
+ send_button_, add_button_, attachments_button_,
+ more_button_}) {
if (button != nullptr) button->setToolButtonStyle(style);
}
}
@@ -1182,8 +1295,8 @@ void EMailPageView::apply_content_lock() {
body_edit_->setReadOnly(locked);
// Attaching and removing change the message; saving a part out does not.
- add_button_->setEnabled(!locked);
- remove_button_->setEnabled(!locked);
+ // The attachment row decides from the same lock.
+ if (attachment_box_ != nullptr) apply_attachment_bar();
// The raw editor writes straight into the document, so leaving it writable
// would be a hole right through the lock. Forensic mode locks it outright;
@@ -1320,6 +1433,8 @@ void EMailPageView::SetForensicMode(bool on) {
if (on) dirty_ = false;
apply_content_lock();
+ // The lock's status label, and whether More still offers it.
+ apply_action_bar();
}
void EMailPageView::mark_dirty() {
@@ -2121,51 +2236,18 @@ void EMailPageView::refresh_body_view() {
// Structure, Security or Headers tab is enough -- and gating on that made
// these three switch themselves on partway through writing a message, with
// nothing to reply to.
- const bool is_message = document_is_received_;
-
- // Every action stays where it is and is turned off instead of taken away. A
- // control that disappears sends the user looking for a feature they think
- // they have lost; one that is merely grey, with a tooltip, tells them what
- // to do next. The reasons here -- no account, no recipient, nothing parsed
- // yet -- are all things they can act on once told.
- send_button_->setVisible(true);
- refresh_send_state();
-
- const auto not_a_message =
- tr("This is a draft you are still writing, not a message that was "
- "received, so there is nothing here to act on yet.");
-
- reply_button_->setVisible(true);
- reply_all_button_->setVisible(true);
- forward_button_->setVisible(true);
- forensic_toggle_->setVisible(true);
- if (action_separator_ != nullptr) action_separator_->setVisible(true);
-
- for (auto* button : {reply_button_, reply_all_button_, forward_button_}) {
- set_action_available(button, is_message, not_a_message);
- }
-
- // Read-only is offered only where it CHANGES something. A signed or
- // encrypted message is already locked by content_lock(), so on those -- the
- // common case in this program -- the toggle appeared to do nothing at all,
- // which is how a real protection came to look like a dead control.
+ // Which actions are on the row, and which are in More, follows what the
+ // document is: a received message is answered, a draft is sent. Neither
+ // shows the other's actions greyed out; see EMailActionBar().
//
- // What it uniquely does, and only on a plain received message: it locks the
- // Raw Source tab outright, and it stops SaveToSource() rebuilding the
- // message. That second one is the point. A rebuild rewrites header order and
- // encodings, and for a message whose HEADERS are the evidence -- a forged
- // Received chain, say -- one stray keystroke and a save would destroy the
- // very thing the message was opened to examine.
- if (!is_message) {
- set_action_available(forensic_toggle_, false, not_a_message);
- } else if (security_state_ != EMailSecurityState::kPLAIN && !forensic_) {
- set_action_available(
- forensic_toggle_, false,
- tr("This message cannot be edited anyway: it is signed or encrypted, "
- "so it is already protected from being rewritten."));
- } else {
- set_action_available(forensic_toggle_, true, {});
- }
+ // Read-only is offered only where it CHANGES something. What it uniquely
+ // does, and only on a plain received message: it locks the Raw Source tab
+ // outright, and it stops SaveToSource() rebuilding the message -- which
+ // for a message whose HEADERS are the evidence (a forged Received chain,
+ // say) is the point. On a signed or encrypted message content_lock()
+ // already protects it, so there the lock is simply not offered.
+ refresh_send_state();
+ apply_action_bar();
// Ciphertext is not text the user can read or edit, so it is not offered as
// either. Deliberately after the block above: Reply and Forward derive a new
@@ -2235,18 +2317,18 @@ auto EMailPageView::eventFilter(QObject* watched, QEvent* event) -> bool {
* where it is and its tooltip says what is missing, which is the only thing a
* disabled control can still do.
*/
-void EMailPageView::set_action_available(QToolButton* button, bool available,
+void EMailPageView::set_action_available(QAction* action, bool available,
const QString& why) {
- if (button == nullptr) return;
+ if (action == nullptr) return;
// Captured on first use rather than at construction, so this cannot fall out
- // of step with whatever wording make_action() gave the button.
- if (!action_tooltips_.contains(button)) {
- action_tooltips_.insert(button, button->toolTip());
+ // of step with whatever wording make_command() gave the action.
+ if (!action_tooltips_.contains(action)) {
+ action_tooltips_.insert(action, action->toolTip());
}
- button->setEnabled(available);
- button->setToolTip(available ? action_tooltips_.value(button) : why);
+ action->setEnabled(available);
+ action->setToolTip(available ? action_tooltips_.value(action) : why);
}
void EMailPageView::refresh_send_state() {
@@ -2300,7 +2382,7 @@ void EMailPageView::refresh_send_state() {
}
}
- set_action_available(send_button_, blocker.isEmpty(), blocker);
+ set_action_available(send_act_, blocker.isEmpty(), blocker);
}
/**
@@ -2406,66 +2488,15 @@ void EMailPageView::paint_security_button() {
}
void EMailPageView::refresh_security_button() {
- QString text;
- QString icon;
-
const bool encrypted =
security_state_ == EMailSecurityState::kENCRYPTED ||
security_state_ == EMailSecurityState::kSIGNED_ENCRYPTED;
- switch (security_badge()) {
- case EMailBadgeState::kENCRYPTED_ONLY:
- text = tr("Encrypted");
- icon = ":/icons/lock.png";
- break;
- case EMailBadgeState::kSIGNED_UNVERIFIED:
- // What is actually known at this point: the message CARRIES a
- // signature. Whether it is any good is a separate question that has
- // not been asked yet, and the wording must not answer it.
- text = encrypted ? tr("Encrypted, signature not checked")
- : tr("Signature not checked");
- icon = ":/icons/signature.png";
- break;
- case EMailBadgeState::kSIGNED_GOOD:
- text = encrypted ? tr("Encrypted, signature verified")
- : tr("Signature verified");
- icon = ":/icons/signature.png";
- break;
- case EMailBadgeState::kSIGNED_MISMATCH:
- text = tr("Signed by a different address");
- icon = ":/icons/warning.png";
- break;
- case EMailBadgeState::kSIGNED_EXPIRED:
- text = tr("Signature or key expired");
- icon = ":/icons/warning.png";
- break;
- case EMailBadgeState::kSIGNED_UNKNOWN_KEY:
- text = tr("Signed by an unknown key");
- icon = ":/icons/warning.png";
- break;
- case EMailBadgeState::kSIGNED_BAD:
- text = tr("Bad signature");
- icon = ":/icons/warning.png";
- break;
- case EMailBadgeState::kSIGNED_ERROR:
- // Not the same claim as a bad signature, and not the same as one nobody
- // has looked at. The check was made and could not produce an answer,
- // which is the one outcome trying again might change.
- text = tr("Signature could not be checked");
- icon = ":/icons/warning.png";
- break;
- case EMailBadgeState::kMALFORMED:
- text = tr("Malformed OpenPGP structure");
- icon = ":/icons/warning.png";
- break;
- case EMailBadgeState::kNOT_PROTECTED:
- // Stated plainly and quietly. An unprotected message is the ordinary
- // case, not a fault, and painting it as a warning would train the user
- // to ignore the one that matters.
- text = tr("Not signed or encrypted");
- icon = ":/icons/email.png";
- break;
- }
+ // The wording is the one rule in EMailSecurityStatus(), so what the row says
+ // about a message is checked, not merely written down here.
+ const auto status = EMailSecurityStatus(security_badge(), encrypted);
+ const auto& text = status.text;
+ const auto& icon = status.icon;
// Always shown, including on a tab the user is still composing. This is not
// only a statement about what the message IS -- it is also where Sign and
@@ -2840,17 +2871,7 @@ void EMailPageView::refresh_attachments() {
}
const bool has_any = !message_.attachments.isEmpty();
-
- // An empty table is a large blank box that reads as a fault and takes the
- // room the body wants, so the list only exists once there is something in
- // it. "Attach File..." stays available either way.
- attachment_list_->setVisible(has_any);
- attachment_heading_->setVisible(has_any);
- if (has_any) {
- const auto count = static_cast<int>(message_.attachments.size());
- attachment_heading_->setText(count == 1 ? tr("1 attachment")
- : tr("%1 attachments").arg(count));
- }
+ attachment_heading_->setText(attachment_summary());
// "Signed" is only an answer on a message that was received and verified.
// While composing, nothing has been signed yet and a column of "no" would
@@ -2881,11 +2902,124 @@ void EMailPageView::refresh_attachments() {
slot_selection_changed();
}
+auto EMailPageView::attachment_summary() const -> QString {
+ const auto count = static_cast<int>(message_.attachments.size());
+ if (count == 0) return {};
+ if (count == 1) {
+ // Complete on its own: name, size and type, so one part never needs the
+ // table to be told apart.
+ const auto& att = message_.attachments.first();
+ return tr("%1 · %2 · %3")
+ .arg(SanitizeAttachmentFileName(att.filename, att.mime_type),
+ HumanSize(att.data.size()), att.mime_type);
+ }
+ qint64 total = 0;
+ for (const auto& att : message_.attachments) total += att.data.size();
+ return tr("%1 files · %2").arg(count).arg(HumanSize(total));
+}
+
+auto EMailPageView::attachment_list_open() const -> bool {
+ return attachments_expanded_ && !message_.attachments.isEmpty();
+}
+
+auto EMailPageView::attachment_target_rows() const -> QList<int> {
+ QList<int> selected;
+ for (auto* item : attachment_list_->selectedItems()) {
+ selected.append(attachment_list_->indexOfTopLevelItem(item));
+ }
+ // The section's own decision, not the widget's visibility: a list inside a
+ // hidden raw-source page is still the list the user left open.
+ return EMailAttachmentTargets(static_cast<int>(message_.attachments.size()),
+ selected, attachment_list_open());
+}
+
+void EMailPageView::apply_attachment_bar() {
+ if (attachment_box_ == nullptr) return;
+ const auto locked = content_lock() != EMailLockReason::kNONE;
+ const auto s = EMailAttachmentBar(
+ {static_cast<int>(message_.attachments.size()),
+ static_cast<int>(attachment_list_->selectedItems().size()), !locked,
+ attachments_expanded_});
+
+ // The raw source mode takes the whole section away, whatever it would say.
+ attachment_box_->setVisible(s.section_visible && !source_mode_);
+ attachment_list_->setVisible(s.list_visible);
+
+ // The menu is the one place the operations are. What cannot apply in this
+ // mode is left out; what applies but has nothing chosen is greyed.
+ attachment_open_act_->setVisible(s.show_open);
+ attachment_save_act_->setVisible(s.show_save);
+ attachment_save_all_act_->setVisible(s.show_save_all);
+ attachment_edit_separator_->setVisible(s.show_attach || s.show_remove);
+ attachment_attach_act_->setVisible(s.show_attach);
+ attachment_remove_act_->setVisible(s.show_remove);
+ attachment_open_act_->setEnabled(s.can_open);
+ attachment_save_act_->setEnabled(s.can_save);
+ attachment_save_all_act_->setEnabled(s.can_save_all);
+ attachment_attach_act_->setEnabled(s.can_attach);
+ attachment_remove_act_->setEnabled(s.can_remove);
+
+ attachment_toggle_->setVisible(s.toggle_visible);
+ attachment_toggle_->setText(s.list_visible ? tr("Collapse") : tr("Expand"));
+ attachment_toggle_->setToolTip(s.list_visible ? tr("Show only the summary")
+ : tr("Show every attachment"));
+ attachment_toggle_->setIcon(EMailChevronIcon(palette(), !s.list_visible));
+
+ // The row's Attachments menu and Attach button follow the same counts.
+ apply_action_bar();
+}
+
+void EMailPageView::apply_action_bar() {
+ if (reply_button_ == nullptr) return;
+
+ // A plain received message is the only one the lock changes anything on:
+ // a signed or encrypted one is already protected by content_lock(). And a
+ // message nobody has sent yet has no Message-ID -- one is only minted when
+ // it goes out -- which is what tells a reply being written from the message
+ // it answers.
+ const bool is_message = document_is_received_;
+ const bool draft = !is_message || message_.message_id.trimmed().isEmpty();
+ const auto s = EMailActionBar(
+ {is_message, draft, content_lock() == EMailLockReason::kNONE,
+ is_message && security_state_ == EMailSecurityState::kPLAIN, forensic_,
+ static_cast<int>(message_.attachments.size())});
+
+ for (auto* button : {reply_button_, reply_all_button_, forward_button_}) {
+ button->setVisible(s.reply_visible);
+ }
+ send_button_->setVisible(s.send_visible);
+ add_button_->setVisible(s.attach_visible);
+ attachments_button_->setVisible(s.attachments_menu_visible);
+
+ details_act_->setVisible(s.more_details);
+ // Each command is in one of two places, on the row or in More, and its
+ // shortcut works in both. Moved by taking the entry out of the menu rather
+ // than by hiding the action: a hidden action's shortcut stops working too.
+ for (auto* action : {reply_act_, reply_all_act_, forward_act_, send_act_}) {
+ more_menu_->removeAction(action);
+ }
+ if (s.more_reply) {
+ for (auto* action : {reply_act_, reply_all_act_, forward_act_}) {
+ more_menu_->insertAction(more_answer_separator_, action);
+ }
+ }
+ more_answer_separator_->setVisible(s.more_reply);
+ if (s.more_send) more_menu_->insertAction(more_lock_separator_, send_act_);
+ more_lock_separator_->setVisible(s.more_read_only);
+ // The lock has no shortcut, so this one may simply be hidden.
+ forensic_act_->setVisible(s.more_read_only);
+ if (forensic_act_->isChecked() != forensic_) {
+ const QSignalBlocker blocker(forensic_act_);
+ forensic_act_->setChecked(forensic_);
+ }
+
+ read_only_label_->setVisible(s.read_only_label);
+}
+
void EMailPageView::report_attachment_status(const QString& note) {
// Said where the attachments are, rather than in a dialog the user has to
// dismiss: this is the outcome of something they just asked for and were
// watching, not news that has to interrupt them.
- attachment_heading_->setVisible(true);
attachment_heading_->setText(note);
// Back to describing the list afterwards. The heading is the list's label
@@ -3065,25 +3199,10 @@ void EMailPageView::WipeContent() {
}
void EMailPageView::slot_selection_changed() {
- const auto selected = attachment_list_->selectedItems().size();
- const auto total = attachment_list_->topLevelItemCount();
-
- // Selection alone is not enough to remove a part: a signed or encrypted
- // message does not change, and this runs AFTER apply_content_lock() on every
- // refresh, so without the lock here it handed the button straight back.
- // Saving a part out does not change the message, so those two only follow
- // the selection.
- const auto locked = content_lock() != EMailLockReason::kNONE;
- remove_button_->setEnabled(selected > 0 && !locked);
- save_button_->setEnabled(selected > 0);
- save_all_button_->setEnabled(total > 0);
-
- // Hidden rather than permanently greyed out: on a message with no
- // attachments these two can never apply, and a row of dead buttons is just
- // clutter.
- save_button_->setVisible(total > 0);
- save_all_button_->setVisible(total > 0);
- remove_button_->setVisible(total > 0);
+ // What is shown, and what each action would act on, is one decision; see
+ // EMailAttachmentBar(). It also carries the lock, which runs before this on
+ // every refresh and must not be undone by a selection.
+ apply_attachment_bar();
}
void EMailPageView::slot_add_attachment() {
@@ -3361,14 +3480,9 @@ void EMailPageView::slot_remove_attachment() {
// adding one. The button is already disabled; this is the backstop.
if (content_lock() != EMailLockReason::kNONE) return;
- const auto selected = attachment_list_->selectedItems();
- if (selected.isEmpty()) return;
-
- // Collect indices first: removing while iterating would shift the rest.
- QList<int> rows;
- for (auto* item : selected) {
- rows.append(attachment_list_->indexOfTopLevelItem(item));
- }
+ // Indices first: removing while iterating would shift the rest.
+ auto rows = attachment_target_rows();
+ if (rows.isEmpty()) return;
std::sort(rows.begin(), rows.end(), std::greater<>());
// On a message that arrived from somewhere else, these bytes may be the only
@@ -3435,12 +3549,11 @@ auto EMailPageView::write_attachment(const EMailAttachment& att,
}
void EMailPageView::slot_save_attachment() {
- const auto selected = attachment_list_->selectedItems();
- if (selected.isEmpty()) return;
+ const auto rows = attachment_target_rows();
+ if (rows.isEmpty()) return;
QList<EMailAttachment> chosen;
- for (auto* item : selected) {
- const auto row = attachment_list_->indexOfTopLevelItem(item);
+ for (const auto row : rows) {
if (row >= 0 && row < message_.attachments.size()) {
chosen.append(message_.attachments[row]);
}