diff options
| -rw-r--r-- | CHANGELOG.md | 11 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | 105 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md | 95 | ||||
| -rw-r--r-- | src/composewindow.cpp | 216 | ||||
| -rw-r--r-- | src/composewindow.h | 23 | ||||
| -rw-r--r-- | tests/test_mainwindow.cpp | 241 | ||||
| -rw-r--r-- | translations/qtmaildir_it_IT.ts | 47 |
7 files changed, 602 insertions, 136 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index da2156c..c0a567c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,14 @@ point at which they are stable. reply follows what the message being answered used. - Drafts autosave to the account's `drafts` folder as ordinary Maildir files, so mbsync carries them to the server and another client can pick one up. +- **The composer is laid out by scope.** Send is a large button beside the + headers rather than one more entry in a row of formatting buttons; the + formatting controls are icons on their own bar directly above the editor, + with Attach and Send as HTML at its right end; Remove attachment appears + beside the attachment list, and only once something is attached. +- Cc and Bcc hide behind a disclosure next to To:, and reveal themselves + whenever either already carries a value, so a reply or a draft never hides + a recipient. - **A button bar over the message pane.** Reply and Forward sit directly above the message, with Toggle HTML at the right end. Forward had been reachable only from the Message menu. @@ -48,6 +56,9 @@ point at which they are stable. ### Changed +- "Also send a formatted copy" is now "Send as HTML", which says what it + does. It moved from a checkbox above the editor to the right end of the + formatting bar. - The main toolbar keeps the actions that need no particular message (Compose, Sync, Archive, Delete, Mark all read, Undo). Reply moves to the new message-pane bar, where Forward joins it. diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md index 3c1c8fd..e5a046e 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md @@ -7215,3 +7215,108 @@ key. A test asserting the resulting 28 alone would stop meaning anything the moment it did, so it asserts the relation as well. --- + +## 142. The composer's formatting buttons share a toolbar with Send and Attach + +**Observed.** The user reads the composer's top row as a menu bar that is not +one, and asks for the formatting controls to move down beside the HTML +checkbox, directly above the editor. + +**Cause, verified 2026-08-23.** `composewindow.cpp:326-397` builds ONE +`addToolBar`, which carries Bold, Italic, Code, the heading and list actions, +Link and Quote, then a separator, then Attach, Remove attachment and Send. +Three different scopes in one row: text formatting, message composition, and +the terminal action. + +**Approach.** Split it. The formatting half moves to a row directly above the +editor, where the text it formats is; Attach, Remove attachment and Send stay +in the window's own toolbar. The HTML checkbox already sits under the editor +and is the anchor the user names. + +**Constraints.** `setInputsEnabled()` disables `m_formatToolbar` wholesale +during a send (`composewindow.cpp:692`), so a split needs both halves disabled, +and a test for the send path that asserts on only one of them would pass +against a live Attach button during a send. + +**Outcome, and the constraint was right twice over.** The existing +`disablingInputsCoversEveryFieldAndTheToolbar` did assert on the toolbar as +one widget, exactly as predicted, so it was strengthened to name every +control BEFORE the split. It then failed on the first build, reporting +`compose_attach` live during a countdown, which is the fault the entry +describes: a file appended to `m_attachments` after `MessageBuilder` has run +is either silently dropped or added to bytes already handed to the send +command, and neither reports anything. + +**The second fault was not predicted and is worth more than the first.** +With every control named, the test then failed on `format_bold`. Disabling a +`QToolBar` greys its buttons but leaves each `QAction` ENABLED, so the +keyboard shortcut still fires: `Ctrl+B` during a send would have edited a +message already being built, through a button that looked unavailable. The +old code was never wrong about this only because the whole row was one +widget whose actions nothing else could reach. `setInputsEnabled()` now walks +`m_formatToolbar->actions()` as well as disabling the bar. + +--- + +## 143. The formatting buttons are text where every editor uses icons + +**Observed.** The user asks for icon-only formatting buttons, "like any other +text editor". + +**Cause.** They were built as text actions, and the composer's toolbar has no +icons at all. + +**Approach.** `QIcon::fromTheme` per CLAUDE.md's rule that chrome is the +system's, keeping the current text as the tooltip so nothing becomes +unnameable. Cheap once item 142 has moved the row, and awkward before, since +the same row would then mix icon-only formatting with text Send and Attach. + +**Constraints.** `format-text-bold` and its siblings are standard freedesktop +names, but a theme may not carry all of them; an action with no icon must fall +back to its text rather than rendering as an empty button. Icon-only is also +the state where the tooltip stops being decoration, so every one needs to be +right. + +--- + +## 144. "Also send a formatted copy" is prominent and says nothing + +**Observed.** The user's note: "I suppose it means 'format/send as html', but +that flag is secondary, doesn't need to be so prominent." + +**Cause, verified 2026-08-23.** `composewindow.cpp:265`. The label describes a +mechanism ("a formatted copy") without naming it, so the reader has to infer +that "formatted" means HTML and that "copy" means an additional MIME part +rather than a second message. + +**Approach.** Two independent halves, and they can ship separately. Say what it +does: something closer to "Send an HTML version as well". And demote it: it is +a per-message override of a config default (`[compose] send_html`), which is +secondary to writing the message. + +**Constraints.** The string is translated, so changing it makes the Italian +entry stale; `ctest -R translations` fails on an untranslated string, which is +the intended safety net rather than an obstacle. Nothing matches on this text, +so CLAUDE.md's "translating a string something matches on" trap does not apply. + +--- + +## 145. Cc and Bcc are permanent rows on every composer + +**Observed.** The user asks for them hidden behind a disclosure next to the To: +field. + +**Cause, verified 2026-08-23.** `composewindow.cpp:251` and `255` add both as +unconditional form rows. Most messages address neither, so two of the four +header rows are usually empty. + +**Approach.** A disclosure beside To: that reveals both together. They expand +automatically, and stay expanded, whenever either already carries a value: a +reply that carries Cc, or a reopened draft, must not hide a recipient the +message is actually addressed to. + +**Constraints.** That auto-expansion is the load-bearing half. A hidden field +holding an address is a message going somewhere the sender cannot see, which is +worse than the clutter this removes. The seeding runs before `buildUi()`'s +`markDirty()` connections per the constructor's ordering comment, so whatever +decides the initial state has to read the seeded values rather than the widgets. diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md index 5a42fb3..2227019 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability.md @@ -211,10 +211,10 @@ taking that too literally. | 139 | Forward is reachable only from the Message menu | discoverability | XS | **done** 2026-08-24, unreleased, inside 140/141 as that entry said it would be. Forward is on the message pane's own bar with Compose and Reply | | 140 | Compose, Reply and Forward belong over the message pane, not on the main toolbar | presentation | M | **done** 2026-08-24, unreleased, with 139 and 141, then REVISED the same day after the user looked at it. Reply and Forward move; **Compose stays on the main toolbar**, because the split that survives contact is what the action NEEDS rather than what it is about, and composing needs no message at all. The moved actions leave the toolbar rather than gaining a second home. Same `QAction` objects shown twice over, never copies, so enablement and the menu entries stay single-sourced | | 141 | The message pane has no button bar of its own | presentation | M | **done** 2026-08-24, unreleased, with 139 and 140. The design question the entry flagged was settled with the user: message actions left, view controls right, separated by an expanding spacer, with `toggle_html` the first of the latter. It sits directly above the web view, BELOW the subject and details rows, which was the user's correction after seeing it at the top of the pane read as window chrome. Icons are 7/8 of `toolbar_icon_size` (28 against the user's 32), derived so the relation survives a change to that key. `MessageView::setBarActions()` is the seam, so the pane still knows nothing about `MainWindow`'s action map. Two traps: a toolbar has no `addStretch()`, and `noTwoActionsShareAnIcon` took an UNNAMED `findChild<QToolBar*>` which now has two candidates, so it is pinned to `main_toolbar` or it would assert against the wrong bar and pass while the rule went unchecked | -| 142 | The composer's formatting buttons share a toolbar with Send and Attach | presentation | S | open, 2026-08-23, from the notes. Verified: one `addToolBar` carries Bold through Quote, then Attach, Remove attachment and Send. The user reads the row as a menu bar that is not one. Move the formatting half down to sit directly above the editor, beside the HTML checkbox | -| 143 | The formatting buttons are text, where every editor uses icons | presentation | XS | open, 2026-08-23, from the notes. Follows 142, and cheap once the row moves. `QIcon::fromTheme` per CLAUDE.md's chrome rule, with the text kept as the tooltip | -| 144 | "Also send a formatted copy" is prominent and does not say what it does | presentation | XS | open, 2026-08-23, from the notes. It means "send an HTML part as well as plain text", which the label never says. Secondary to writing the message, so it should read as such | -| 145 | Cc and Bcc are permanent rows on every composer | presentation | S | open, 2026-08-23, from the notes. Verified: both are unconditional `form->addRow` calls. Most messages use neither. Collapse behind a disclosure next to To:, expanded automatically when a draft or a reply already carries a value | +| 142 | The composer's formatting buttons share a toolbar with Send and Attach | presentation | S | **done** 2026-08-24, unreleased, with 143/144/145 to the user's own layout. The one `addToolBar` is gone: the composer has no window toolbar at all. Formatting is a `QToolBar` WIDGET in the central column directly above the editor, Send is a big icon-above-text button beside the headers, Attach and the HTML toggle ride the right end of the editor bar, Remove attachment sits with the attachment list. The constraint this entry named came true: the strengthened send-lock test caught Attach live during a countdown, and then a SECOND fault the entry did not predict, see the section | +| 143 | The formatting buttons are text, where every editor uses icons | presentation | XS | **done** 2026-08-24, unreleased, inside 142. `QIcon::fromTheme` per CLAUDE.md's chrome rule, the words kept as the tooltip, and an action whose theme lacks the name keeps its text rather than rendering an empty button | +| 144 | "Also send a formatted copy" is prominent and does not say what it does | presentation | XS | **done** 2026-08-24, unreleased, inside 142. "Send as HTML", icon and text, alone at the right end of the editor bar where it reads as a control of the editor rather than as a formatting button. The Italian entry was refreshed with it, and `lrelease` reports 477 finished, 0 unfinished | +| 145 | Cc and Bcc are permanent rows on every composer | presentation | S | **done** 2026-08-24, unreleased, inside 142. A `QToolButton` disclosure beside To:. `revealCcBccIfUsed()` is the load-bearing half the entry called for: it only ever SHOWS, never hides, so nothing but the user's own click can make a field holding an address invisible. `ComposeContext` carries no `bcc` at all, so the seeded-Bcc case can only arrive from a reopened draft, which is what its test drives. The LABEL is hidden with each field: a `QFormLayout` holds the two as separate items, so hiding the line edit alone strands a `Cc:` over empty space | | 146 | The unsynced-changes count cannot be opened to see what it counts | information | S | **duplicate of 119**, recorded 2026-08-23 from the notes. Same request, and 119 already carries the blocker: one of the four things the count sums holds no message ids, so a list cannot be complete without changing how the count is kept | | 147 | Toggle unread reads the same whichever way it will go | presentation | S | **duplicate of 99**, recorded 2026-08-23 from the notes. The notes ask for exactly what 99 describes: "Mark as read" on an unread message and the reverse. 99 already records that the label is harder than it looks, since a multi-row selection has no single direction | | 148 | Ctrl+W does not close the composer | discoverability | XS | open, 2026-08-23, from the notes. Verified: nothing binds `Ctrl+W` anywhere, and the composer has no close action of its own. Belongs with item 21's table rather than bound in isolation | @@ -1374,90 +1374,3 @@ what a drafts folder is; they are separate readers and neither should start importing the other. --- - -## 142. The composer's formatting buttons share a toolbar with Send and Attach - -**Observed.** The user reads the composer's top row as a menu bar that is not -one, and asks for the formatting controls to move down beside the HTML -checkbox, directly above the editor. - -**Cause, verified 2026-08-23.** `composewindow.cpp:326-397` builds ONE -`addToolBar`, which carries Bold, Italic, Code, the heading and list actions, -Link and Quote, then a separator, then Attach, Remove attachment and Send. -Three different scopes in one row: text formatting, message composition, and -the terminal action. - -**Approach.** Split it. The formatting half moves to a row directly above the -editor, where the text it formats is; Attach, Remove attachment and Send stay -in the window's own toolbar. The HTML checkbox already sits under the editor -and is the anchor the user names. - -**Constraints.** `setInputsEnabled()` disables `m_formatToolbar` wholesale -during a send (`composewindow.cpp:692`), so a split needs both halves disabled, -and a test for the send path that asserts on only one of them would pass -against a live Attach button during a send. - ---- - -## 143. The formatting buttons are text where every editor uses icons - -**Observed.** The user asks for icon-only formatting buttons, "like any other -text editor". - -**Cause.** They were built as text actions, and the composer's toolbar has no -icons at all. - -**Approach.** `QIcon::fromTheme` per CLAUDE.md's rule that chrome is the -system's, keeping the current text as the tooltip so nothing becomes -unnameable. Cheap once item 142 has moved the row, and awkward before, since -the same row would then mix icon-only formatting with text Send and Attach. - -**Constraints.** `format-text-bold` and its siblings are standard freedesktop -names, but a theme may not carry all of them; an action with no icon must fall -back to its text rather than rendering as an empty button. Icon-only is also -the state where the tooltip stops being decoration, so every one needs to be -right. - ---- - -## 144. "Also send a formatted copy" is prominent and says nothing - -**Observed.** The user's note: "I suppose it means 'format/send as html', but -that flag is secondary, doesn't need to be so prominent." - -**Cause, verified 2026-08-23.** `composewindow.cpp:265`. The label describes a -mechanism ("a formatted copy") without naming it, so the reader has to infer -that "formatted" means HTML and that "copy" means an additional MIME part -rather than a second message. - -**Approach.** Two independent halves, and they can ship separately. Say what it -does: something closer to "Send an HTML version as well". And demote it: it is -a per-message override of a config default (`[compose] send_html`), which is -secondary to writing the message. - -**Constraints.** The string is translated, so changing it makes the Italian -entry stale; `ctest -R translations` fails on an untranslated string, which is -the intended safety net rather than an obstacle. Nothing matches on this text, -so CLAUDE.md's "translating a string something matches on" trap does not apply. - ---- - -## 145. Cc and Bcc are permanent rows on every composer - -**Observed.** The user asks for them hidden behind a disclosure next to the To: -field. - -**Cause, verified 2026-08-23.** `composewindow.cpp:251` and `255` add both as -unconditional form rows. Most messages address neither, so two of the four -header rows are usually empty. - -**Approach.** A disclosure beside To: that reveals both together. They expand -automatically, and stay expanded, whenever either already carries a value: a -reply that carries Cc, or a reopened draft, must not hide a recipient the -message is actually addressed to. - -**Constraints.** That auto-expansion is the load-bearing half. A hidden field -holding an address is a message going somewhere the sender cannot see, which is -worse than the clutter this removes. The seeding runs before `buildUi()`'s -`markDirty()` connections per the constructor's ordering comment, so whatever -decides the initial state has to read the seeded values rather than the widgets. diff --git a/src/composewindow.cpp b/src/composewindow.cpp index 1c9b492..ccf606e 100644 --- a/src/composewindow.cpp +++ b/src/composewindow.cpp @@ -46,6 +46,7 @@ #include <QTextCursor> #include <QTimer> #include <QToolBar> +#include <QToolButton> #include <QVBoxLayout> #include <QWidget> @@ -141,6 +142,10 @@ ComposeWindow::ComposeWindow(const ComposeContext &context, // before the user has typed anything; a composer opened and closed at once // would then write a draft nobody asked for. The timer is stopped as well // as the flag cleared, since markDirty() started it. + // After seedFields(): a reply that carries Cc, or a draft that carries + // either, must show what the message is addressed to. + revealCcBccIfUsed(); + m_dirty = false; m_autosaveTimer->stop(); @@ -244,48 +249,106 @@ void ComposeWindow::buildUi() m_banner->hide(); layout->addWidget(m_banner); + // The headers take the left, Send the right. Send is the terminal action + // and carries the weight to match, rather than sitting as one more entry + // in a row of formatting buttons (item 142). + auto *headerRow = new QHBoxLayout; auto *form = new QFormLayout; m_from = new QComboBox(central); m_from->setObjectName(QStringLiteral("from")); form->addRow(tr("From:"), m_from); + // To, with the Cc/Bcc disclosure beside it: the two hidden fields are + // revealed from the row they belong to. m_to = new QLineEdit(central); m_to->setObjectName(QStringLiteral("to")); - form->addRow(tr("To:"), m_to); - + auto *toRow = new QWidget(central); + auto *toLayout = new QHBoxLayout(toRow); + toLayout->setContentsMargins(0, 0, 0, 0); + toLayout->addWidget(m_to, 1); + + m_ccBccDisclosure = new QToolButton(toRow); + m_ccBccDisclosure->setObjectName(QStringLiteral("ccBccDisclosure")); + m_ccBccDisclosure->setText(tr("Cc/Bcc")); + m_ccBccDisclosure->setToolTip(tr("Show or hide the Cc and Bcc fields")); + m_ccBccDisclosure->setCheckable(true); + m_ccBccDisclosure->setArrowType(Qt::DownArrow); + m_ccBccDisclosure->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + toLayout->addWidget(m_ccBccDisclosure); + form->addRow(tr("To:"), toRow); + + // Cc and Bcc are hidden by default (item 145). The LABEL has to be hidden + // with the field: a QFormLayout keeps the two as separate items, so + // hiding only the QLineEdit leaves a stranded "Cc:" over empty space. m_cc = new QLineEdit(central); m_cc->setObjectName(QStringLiteral("cc")); - form->addRow(tr("Cc:"), m_cc); + auto *ccLabel = new QLabel(tr("Cc:"), central); + form->addRow(ccLabel, m_cc); m_bcc = new QLineEdit(central); m_bcc->setObjectName(QStringLiteral("bcc")); - form->addRow(tr("Bcc:"), m_bcc); + auto *bccLabel = new QLabel(tr("Bcc:"), central); + form->addRow(bccLabel, m_bcc); + + const auto setCcBccVisible = [this, ccLabel, bccLabel](bool visible) { + ccLabel->setVisible(visible); + m_cc->setVisible(visible); + bccLabel->setVisible(visible); + m_bcc->setVisible(visible); + m_ccBccDisclosure->setChecked(visible); + m_ccBccDisclosure->setArrowType(visible ? Qt::UpArrow : Qt::DownArrow); + }; + m_setCcBccVisible = setCcBccVisible; + setCcBccVisible(false); + connect(m_ccBccDisclosure, &QToolButton::toggled, this, + [setCcBccVisible](bool on) { setCcBccVisible(on); }); m_subject = new QLineEdit(central); m_subject->setObjectName(QStringLiteral("subject")); form->addRow(tr("Subject:"), m_subject); - layout->addLayout(form); + headerRow->addLayout(form, 1); - // Labelled for what it does, a formatted copy riding along with the plain - // text, rather than "HTML", which reads as an either/or that it is not. - m_sendHtml = new QCheckBox(tr("Also send a formatted copy"), central); - m_sendHtml->setObjectName(QStringLiteral("sendHtml")); - m_sendHtml->setToolTip( - tr("Sends the message as plain text with a formatted version " - "alongside it. The plain text is what you typed.")); - layout->addWidget(m_sendHtml); + // Icon above text, at the user's choice: a big target, with the word + // removing any doubt about what it does. + m_sendButton = new QToolButton(central); + m_sendButton->setObjectName(QStringLiteral("sendButton")); + m_sendButton->setToolButtonStyle(Qt::ToolButtonTextUnderIcon); + m_sendButton->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); + headerRow->addWidget(m_sendButton, 0); + + layout->addLayout(headerRow); + + // The editor bar is created by buildFormatToolbar(), which runs after + // this, and inserted directly above the body. Recorded here so that + // insertion has a stable index rather than counting widgets. + m_editorBarIndex = layout->count(); m_body = new QPlainTextEdit(central); m_body->setObjectName(QStringLiteral("body")); layout->addWidget(m_body, 1); - m_attachmentList = new QListWidget(central); + // The attachment list, with Remove beside it: the control acts on the + // list, so it lives with it, and both appear only once something is + // attached. A Remove button that can never do anything is worse than no + // button, since it invites a click that reports nothing. + m_attachmentRow = new QWidget(central); + auto *attachmentLayout = new QHBoxLayout(m_attachmentRow); + attachmentLayout->setContentsMargins(0, 0, 0, 0); + + m_detachButton = new QToolButton(m_attachmentRow); + m_detachButton->setObjectName(QStringLiteral("detachButton")); + m_detachButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + attachmentLayout->addWidget(m_detachButton, 0, Qt::AlignTop); + + m_attachmentList = new QListWidget(m_attachmentRow); m_attachmentList->setObjectName(QStringLiteral("attachments")); m_attachmentList->setMaximumHeight(90); - m_attachmentList->hide(); - layout->addWidget(m_attachmentList); + attachmentLayout->addWidget(m_attachmentList, 1); + + m_attachmentRow->hide(); + layout->addWidget(m_attachmentRow); // The send-failure pane, in the shape MainWindow's sync log already has: // a header with a Close button and a read-only QPlainTextEdit under it. A @@ -331,19 +394,47 @@ void ComposeWindow::buildUi() void ComposeWindow::buildFormatToolbar() { - m_formatToolbar = addToolBar(tr("Formatting")); + // A toolbar WIDGET in the central column, not addToolBar(): the row sits + // directly above the text it formats, the way every editor puts it, and a + // window toolbar cannot (item 142). The window has no toolbar at all now. + m_formatToolbar = new QToolBar(centralWidget()); m_formatToolbar->setObjectName(QStringLiteral("formatToolbar")); + m_formatToolbar->setMovable(false); + // Icon-only for the formatting half, per the user's request and item 143. + m_formatToolbar->setToolButtonStyle(Qt::ToolButtonIconOnly); + const int editorIconSize = qMax(16, m_config.toolbarIconSize()); + m_formatToolbar->setIconSize(QSize(editorIconSize, editorIconSize)); + if (auto *column = qobject_cast<QVBoxLayout *>(centralWidget()->layout())) + column->insertWidget(m_editorBarIndex, m_formatToolbar); // A QAction parented to THIS WINDOW, not registered in KeyMap. Its // shortcut is therefore scoped to the composer: Qt dispatches a // WindowShortcut to the active window only, so the main window's Ctrl+B is // untouched and the two namespaces stay apart. These six do not // participate in item 132's reachability rule for the same reason. - const auto addFormat = [this](const QString &name, const QString &text, - const QString &token, - const QKeySequence &shortcut) { + // The theme's icon, with the WORDS kept as the tooltip: icon-only is + // exactly the state where a tooltip stops being decoration, and a theme + // that lacks one of these names must fall back to text rather than render + // an empty button (item 143). + const auto decorate = [](QAction *action, const QString &iconName, + const QString &text) { + action->setToolTip(text); + const QIcon icon = QIcon::fromTheme(iconName); + if (icon.isNull()) + return; + action->setIcon(icon); + // The text stays on the action for the tooltip and for any menu, but + // an icon-only toolbar shows the icon alone. + }; + + const auto addFormat = [this, decorate](const QString &name, + const QString &text, + const QString &iconName, + const QString &token, + const QKeySequence &shortcut) { QAction *action = m_formatToolbar->addAction(text); action->setObjectName(name); + decorate(action, iconName, text); if (!shortcut.isEmpty()) action->setShortcut(shortcut); connect(action, &QAction::triggered, this, @@ -351,18 +442,23 @@ void ComposeWindow::buildFormatToolbar() }; addFormat(QStringLiteral("format_bold"), tr("Bold"), + QStringLiteral("format-text-bold"), QStringLiteral("**"), QKeySequence(QStringLiteral("Ctrl+B"))); addFormat(QStringLiteral("format_italic"), tr("Italic"), + QStringLiteral("format-text-italic"), QStringLiteral("*"), QKeySequence(QStringLiteral("Ctrl+I"))); addFormat(QStringLiteral("format_code"), tr("Code"), + QStringLiteral("format-text-code"), QStringLiteral("`"), QKeySequence(QStringLiteral("Ctrl+`"))); // No shortcut, per the spec's table. addFormat(QStringLiteral("format_strike"), tr("Strikethrough"), + QStringLiteral("format-text-strikethrough"), QStringLiteral("~~"), QKeySequence()); // Link and Quote are not wraps and cannot go through applyFormat(). QAction *link = m_formatToolbar->addAction(tr("Link")); link->setObjectName(QStringLiteral("format_link")); + decorate(link, QStringLiteral("insert-link"), tr("Link")); link->setShortcut(QKeySequence(QStringLiteral("Ctrl+K"))); connect(link, &QAction::triggered, this, [this]() { const QTextCursor cursor = m_body->textCursor(); @@ -373,6 +469,7 @@ void ComposeWindow::buildFormatToolbar() QAction *quote = m_formatToolbar->addAction(tr("Quote")); quote->setObjectName(QStringLiteral("format_quote")); + decorate(quote, QStringLiteral("format-text-blockquote"), tr("Quote")); connect(quote, &QAction::triggered, this, [this]() { const QTextCursor cursor = m_body->textCursor(); applyEdit(MarkdownFormat::quote(m_body->toPlainText(), @@ -380,10 +477,15 @@ void ComposeWindow::buildFormatToolbar() cursor.selectionEnd())); }); - m_formatToolbar->addSeparator(); + // Everything after this sits at the RIGHT of the row, apart from the + // formatting buttons, because none of it formats text. + auto *spacer = new QWidget(m_formatToolbar); + spacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); + m_formatToolbar->addWidget(spacer); m_attachAction = m_formatToolbar->addAction(tr("Attach...")); m_attachAction->setObjectName(QStringLiteral("compose_attach")); + decorate(m_attachAction, QStringLiteral("mail-attachment"), tr("Attach...")); connect(m_attachAction, &QAction::triggered, this, [this]() { const QStringList chosen = QFileDialog::getOpenFileNames( this, tr("Attach files")); @@ -402,10 +504,39 @@ void ComposeWindow::buildFormatToolbar() markDirty(); }); - m_sendAction = m_formatToolbar->addAction(tr("Send")); + // The HTML toggle rides at the right end of the same row, icon AND text + // (item 144). Alone on its side, and worded for what it does rather than + // for the mechanism: "a formatted copy" never said HTML, which is what the + // user had to infer. It is an action on the bar rather than a checkbox + // under it, so it reads as a control of the editor. + m_sendHtml = new QToolButton(m_formatToolbar); + m_sendHtml->setObjectName(QStringLiteral("sendHtml")); + m_sendHtml->setCheckable(true); + m_sendHtml->setText(tr("Send as HTML")); + m_sendHtml->setToolTip( + tr("Sends the message as plain text with an HTML version alongside " + "it. The plain text is what you typed.")); + m_sendHtml->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); + const QIcon htmlIcon = QIcon::fromTheme(QStringLiteral("text-html")); + if (!htmlIcon.isNull()) + m_sendHtml->setIcon(htmlIcon); + m_formatToolbar->addWidget(m_sendHtml); + + // Send is NOT on this row: it is the terminal action, and it lives on the + // button beside the headers. The QAction survives because it carries the + // shortcut and is what the button triggers. + m_sendAction = new QAction(tr("Send"), this); m_sendAction->setObjectName(QStringLiteral("compose_send")); m_sendAction->setShortcut(QKeySequence(QStringLiteral("Ctrl+Return"))); + const QIcon sendIcon = QIcon::fromTheme(QStringLiteral("mail-send")); + if (!sendIcon.isNull()) + m_sendAction->setIcon(sendIcon); connect(m_sendAction, &QAction::triggered, this, &ComposeWindow::send); + addAction(m_sendAction); + m_sendButton->setDefaultAction(m_sendAction); + + // Remove attachment moves to its own button beside the list it acts on. + m_detachButton->setDefaultAction(m_detachAction); } void ComposeWindow::seedFields() @@ -440,6 +571,18 @@ void ComposeWindow::seedFields() : m_config.compose().sendHtml); } +void ComposeWindow::revealCcBccIfUsed() +{ + // Never hides: only the user's own click on the disclosure does that. A + // field holding an address must not become invisible because something + // else changed, which is the whole reason this exists rather than a plain + // "start collapsed". + if (!m_setCcBccVisible) + return; + if (!m_cc->text().trimmed().isEmpty() || !m_bcc->text().trimmed().isEmpty()) + m_setCcBccVisible(true); +} + void ComposeWindow::seedBody() { if (m_context.quotedBody.isEmpty()) @@ -477,7 +620,13 @@ void ComposeWindow::refreshAttachmentList() m_attachmentList->clear(); for (const QString &path : m_attachments) m_attachmentList->addItem(QFileInfo(path).fileName()); - m_attachmentList->setVisible(!m_attachments.isEmpty()); + // The ROW, so Remove goes with the list it acts on. The action's own + // visibility follows, which is what keeps it off screen with nothing + // attached even though it lives on a button rather than in a toolbar. + const bool any = !m_attachments.isEmpty(); + m_attachmentRow->setVisible(any); + if (m_detachAction) + m_detachAction->setVisible(any); } bool ComposeWindow::attachmentNeedsWarning(qint64 size) const @@ -701,6 +850,27 @@ void ComposeWindow::setInputsEnabled(bool enabled) m_sendHtml->setEnabled(enabled); m_attachmentList->setEnabled(enabled); m_formatToolbar->setEnabled(enabled); + + // Every control that used to live in the one toolbar, now that item 142 + // has split it four ways. m_formatToolbar->setEnabled() covered Bold + // through Send when they shared a row; it now reaches only the editor bar, + // and the rest have to be named. An Attach left live during the countdown + // appends to m_attachments after MessageBuilder has already run, so the + // file is either silently dropped or added to bytes already handed to the + // send command, with nothing reported either way. + m_sendButton->setEnabled(enabled); + m_sendAction->setEnabled(enabled); + m_detachButton->setEnabled(enabled); + m_detachAction->setEnabled(enabled); + m_attachAction->setEnabled(enabled); + m_ccBccDisclosure->setEnabled(enabled); + + // The ACTIONS, not only the bar that holds them. Disabling a QToolBar + // greys its buttons but leaves each QAction enabled, so the keyboard + // shortcut still fires: Ctrl+B during a send would edit a message already + // being built, through a button that looks unavailable. + for (QAction *action : m_formatToolbar->actions()) + action->setEnabled(enabled); } void ComposeWindow::showSendFailure(const QString &stderrText) diff --git a/src/composewindow.h b/src/composewindow.h index af50be6..c44fcda 100644 --- a/src/composewindow.h +++ b/src/composewindow.h @@ -19,6 +19,8 @@ #pragma once #include <QMainWindow> + +#include <functional> #include <QStringList> #include <memory> @@ -38,6 +40,8 @@ class QListWidget; class QPlainTextEdit; class QTimer; class QToolBar; +class QAbstractButton; +class QToolButton; class QTemporaryDir; class QWidget; @@ -117,6 +121,15 @@ public: /// server, which this application cannot know, so the user decides. void attachFile(const QString &path); + /// Shows Cc and Bcc when either already carries a value, and leaves them + /// shown. Called after seeding and after a draft is loaded. + /// + /// The load-bearing half of item 145: a hidden field holding an address is + /// a message going somewhere the sender cannot see, which is worse than + /// the clutter the disclosure removes. Never hides: only the user's own + /// click does that. + void revealCcBccIfUsed(); + /// A byte count as a figure a person reads. /// /// Static and public so the formatting is testable without a modal. The @@ -200,12 +213,20 @@ private: std::unique_ptr<QTemporaryDir> m_forwardedParts; QLineEdit *m_to = nullptr; + QWidget *m_attachmentRow = nullptr; + QToolButton *m_detachButton = nullptr; + int m_editorBarIndex = -1; + std::function<void(bool)> m_setCcBccVisible; + QWidget *m_ccRow = nullptr; + QWidget *m_bccRow = nullptr; + QToolButton *m_ccBccDisclosure = nullptr; + QToolButton *m_sendButton = nullptr; QLineEdit *m_cc = nullptr; QLineEdit *m_bcc = nullptr; QLineEdit *m_subject = nullptr; QComboBox *m_from = nullptr; QPlainTextEdit *m_body = nullptr; - QCheckBox *m_sendHtml = nullptr; + QToolButton *m_sendHtml = nullptr; QLabel *m_banner = nullptr; QListWidget *m_attachmentList = nullptr; QWidget *m_sendLogPane = nullptr; diff --git a/tests/test_mainwindow.cpp b/tests/test_mainwindow.cpp index 183e4fa..35f7093 100644 --- a/tests/test_mainwindow.cpp +++ b/tests/test_mainwindow.cpp @@ -476,6 +476,10 @@ private slots: // MainWindow hands it, so a Config written to a temporary INI is the whole // fixture. void aComposerOpensClean(); + void theComposerSplitsItsToolbarByScope(); + void ccAndBccHideBehindADisclosure(); + void ccAndBccAreRevealedWhenTheyCarryAValue(); + void removeAttachmentAppearsOnlyWithAttachments(); void typingMarksTheComposerDirty(); void anAutosaveWritesADraftAndClearsTheDirtyFlag(); void anUnwritableDraftsFolderRaisesThePersistentBanner(); @@ -9432,7 +9436,7 @@ void TestMainWindow::forwardSeedsHtmlFromTheConfigNotTheOriginal() QList<ComposeWindow *> opened = window.openComposersForTest(); QCOMPARE(opened.size(), 1); auto *sendHtml = - opened.first()->findChild<QCheckBox *>(QStringLiteral("sendHtml")); + opened.first()->findChild<QAbstractButton *>(QStringLiteral("sendHtml")); QVERIFY(sendHtml); QVERIFY2(!sendHtml->isChecked(), "Forward seeded sendHtml from the original's HTML part rather " @@ -9446,7 +9450,7 @@ void TestMainWindow::forwardSeedsHtmlFromTheConfigNotTheOriginal() const QList<ComposeWindow *> both = window.openComposersForTest(); QCOMPARE(both.size(), 2); auto *replyHtml = - both.last()->findChild<QCheckBox *>(QStringLiteral("sendHtml")); + both.last()->findChild<QAbstractButton *>(QStringLiteral("sendHtml")); QVERIFY(replyHtml); QVERIFY2(replyHtml->isChecked(), "Reply did not seed sendHtml from the original's HTML part"); @@ -12213,6 +12217,194 @@ ComposeContext newContext() } // namespace +void TestMainWindow::theComposerSplitsItsToolbarByScope() +{ + // Items 142, 143 and 144. One row carried three scopes: text formatting, + // message composition and the terminal action. The user read it as a menu + // bar that is not one. + ComposeFixture fixture; + QVERIFY(fixture.build()); + ComposeContext context = newContext(); + ComposeWindow window(context, fixture.config(), fixture.mailRoot()); + + auto *editorBar = + window.findChild<QToolBar *>(QStringLiteral("formatToolbar")); + auto *body = window.findChild<QPlainTextEdit *>(QStringLiteral("body")); + auto *subject = window.findChild<QLineEdit *>(QStringLiteral("subject")); + QVERIFY(editorBar && body && subject); + + // Directly above the text it formats, and below Subject, which belongs to + // the message rather than to the text. + auto *central = window.centralWidget(); + QVERIFY(central); + auto *column = qobject_cast<QVBoxLayout *>(central->layout()); + QVERIFY2(column, "the composer is not laid out in a vertical column"); + + int barIndex = -1; + int bodyIndex = -1; + for (int i = 0; i < column->count(); ++i) { + QLayoutItem *item = column->itemAt(i); + if (item->widget() == editorBar) + barIndex = i; + else if (item->widget() == body) + bodyIndex = i; + } + QVERIFY2(barIndex >= 0 && bodyIndex >= 0, + "the editor bar or the body is not in the composer's column"); + QVERIFY2(barIndex < bodyIndex, "the editor bar is not above the editor"); + + // Send is NOT in that row any more: it is the terminal action and sits by + // the headers, where its weight belongs. + auto *send = window.findChild<QAction *>(QStringLiteral("compose_send")); + QVERIFY(send); + QVERIFY2(!editorBar->actions().contains(send), + "Send still shares the editor bar with the formatting buttons"); + + auto *sendButton = + window.findChild<QToolButton *>(QStringLiteral("sendButton")); + QVERIFY2(sendButton, "there is no Send button beside the headers"); + QCOMPARE(sendButton->toolButtonStyle(), Qt::ToolButtonTextUnderIcon); + QVERIFY2(sendButton->defaultAction() == send, + "the Send button does not carry the send action itself"); + + // Item 143: the formatting buttons carry icons, and keep their words as + // the tooltip so nothing becomes unnameable in an icon-only row. + for (const QString &name : { QStringLiteral("format_bold"), + QStringLiteral("format_italic"), + QStringLiteral("format_link") }) { + auto *action = window.findChild<QAction *>(name); + QVERIFY2(action, qPrintable(QStringLiteral("no action %1").arg(name))); + QVERIFY2(!action->toolTip().isEmpty(), + qPrintable(QStringLiteral("%1 has no tooltip, so an " + "icon-only button cannot be named") + .arg(name))); + } + + // Item 144: the HTML toggle sits at the right of the same row, apart from + // the formatting buttons, since it is not one. + auto *sendHtml = + window.findChild<QAbstractButton *>(QStringLiteral("sendHtml")); + QVERIFY(sendHtml); +} + +void TestMainWindow::ccAndBccHideBehindADisclosure() +{ + // Item 145. Most messages address neither, so two of four header rows sat + // empty on every composer. + ComposeFixture fixture; + QVERIFY(fixture.build()); + ComposeContext context = newContext(); + context.cc.clear(); + + ComposeWindow window(context, fixture.config(), fixture.mailRoot()); + auto *cc = window.findChild<QLineEdit *>(QStringLiteral("cc")); + auto *bcc = window.findChild<QLineEdit *>(QStringLiteral("bcc")); + auto *disclosure = window.findChild<QAbstractButton *>( + QStringLiteral("ccBccDisclosure")); + QVERIFY(cc && bcc); + QVERIFY2(disclosure, "there is no Cc/Bcc disclosure beside To:"); + + QVERIFY2(cc->isHidden(), "Cc is shown on a composer that carries none"); + QVERIFY2(bcc->isHidden(), "Bcc is shown on a composer that carries none"); + + disclosure->click(); + QVERIFY2(!cc->isHidden() && !bcc->isHidden(), + "the disclosure did not reveal Cc and Bcc"); + + disclosure->click(); + QVERIFY2(cc->isHidden() && bcc->isHidden(), + "the disclosure did not hide Cc and Bcc again"); +} + +void TestMainWindow::ccAndBccAreRevealedWhenTheyCarryAValue() +{ + // The load-bearing half of item 145, and the reason the disclosure is not + // simply "start collapsed". A hidden field holding an address is a message + // going somewhere the sender cannot see, which is worse than the clutter + // the disclosure removes. A reply carrying Cc, or a reopened draft, must + // show what it is addressed to. + ComposeFixture fixture; + QVERIFY(fixture.build()); + + { + ComposeContext context = newContext(); + context.cc = { QStringLiteral("someone@example.org") }; + + ComposeWindow window(context, fixture.config(), fixture.mailRoot()); + auto *cc = window.findChild<QLineEdit *>(QStringLiteral("cc")); + auto *bcc = window.findChild<QLineEdit *>(QStringLiteral("bcc")); + QVERIFY(cc && bcc); + QVERIFY2(!cc->isHidden(), + "a seeded Cc is hidden, so the message goes somewhere the " + "sender cannot see"); + // Both together: they are one disclosure, and revealing half of it + // would leave Bcc hidden while Cc is visible for no stated reason. + QVERIFY2(!bcc->isHidden(), + "Cc was revealed without Bcc, though they share a disclosure"); + } + + // ComposeContext carries no bcc at all: a reply never inherits one, so + // the only way a fresh composer starts with a Bcc is a reopened draft, + // which fills the widget rather than the context. That is also the case + // where hiding it matters most, since a Bcc is the value a sender is + // least able to notice missing. + { + ComposeContext context = newContext(); + context.cc.clear(); + + ComposeWindow window(context, fixture.config(), fixture.mailRoot()); + auto *bcc = window.findChild<QLineEdit *>(QStringLiteral("bcc")); + auto *cc = window.findChild<QLineEdit *>(QStringLiteral("cc")); + QVERIFY(bcc && cc); + QVERIFY(bcc->isHidden()); + + // Setting the text is what a draft load does. + bcc->setText(QStringLiteral("hidden@example.org")); + window.revealCcBccIfUsed(); + QVERIFY2(!bcc->isHidden(), + "a Bcc filled after construction stayed hidden"); + QVERIFY2(!cc->isHidden(), + "Bcc was revealed without Cc, though they share a disclosure"); + } +} + +void TestMainWindow::removeAttachmentAppearsOnlyWithAttachments() +{ + // The user's placement: Remove sits with the list it acts on, and a + // control that can never do anything should not be on screen at all. + ComposeFixture fixture; + QVERIFY(fixture.build()); + ComposeContext context = newContext(); + ComposeWindow window(context, fixture.config(), fixture.mailRoot()); + + auto *detach = window.findChild<QAction *>(QStringLiteral("compose_detach")); + auto *list = window.findChild<QListWidget *>(QStringLiteral("attachments")); + QVERIFY(detach && list); + + // isVisible() on the LIST, not isHidden(): the list is not hidden in its + // own right, its parent row is, and a child of a hidden parent reports + // isHidden() false while being just as invisible. Asserting the wrong one + // fails against correct code. + QVERIFY2(!list->isVisible(), + "the attachment list shows with nothing attached"); + QVERIFY2(!detach->isVisible(), + "Remove attachment is offered with nothing attached"); + + QTemporaryDir dir; + const QString path = dir.filePath(QStringLiteral("note.txt")); + QFile file(path); + QVERIFY(file.open(QIODevice::WriteOnly)); + file.write("x"); + file.close(); + window.attachFile(path); + + QVERIFY2(!list->isHidden(), "the attachment list stayed hidden"); + auto *row = list->parentWidget(); + QVERIFY(row); + QVERIFY2(detach->isVisible(), + "Remove attachment is not offered with a file attached"); +} + void TestMainWindow::aComposerOpensClean() { ComposeFixture fixture; @@ -12808,7 +13000,7 @@ void TestMainWindow::aReplySeedsTheHtmlToggleFromTheOriginal() context.seedHtml = false; ComposeWindow window(context, fixture.config(), fixture.mailRoot()); - auto *toggle = window.findChild<QCheckBox *>(QStringLiteral("sendHtml")); + auto *toggle = window.findChild<QAbstractButton *>(QStringLiteral("sendHtml")); QVERIFY2(toggle, "no send-html toggle"); QVERIFY2(!toggle->isChecked(), "a reply seeded from config rather than from the original"); @@ -12824,7 +13016,7 @@ void TestMainWindow::aReplySeedsTheHtmlToggleFromTheOriginal() ComposeWindow second(htmlReply, plain.config(), plain.mailRoot()); auto *secondToggle = - second.findChild<QCheckBox *>(QStringLiteral("sendHtml")); + second.findChild<QAbstractButton *>(QStringLiteral("sendHtml")); QVERIFY(secondToggle); QVERIFY2(secondToggle->isChecked(), "a reply-all ignored an HTML original"); @@ -12843,7 +13035,7 @@ void TestMainWindow::aNewMessageSeedsTheHtmlToggleFromConfig() context.seedHtml = true; ComposeWindow window(context, off.config(), off.mailRoot()); - auto *toggle = window.findChild<QCheckBox *>(QStringLiteral("sendHtml")); + auto *toggle = window.findChild<QAbstractButton *>(QStringLiteral("sendHtml")); QVERIFY(toggle); QVERIFY2(!toggle->isChecked(), "a New message ignored [compose] send_html"); @@ -12856,7 +13048,7 @@ void TestMainWindow::aNewMessageSeedsTheHtmlToggleFromConfig() ComposeWindow second(forward, on.config(), on.mailRoot()); auto *secondToggle = - second.findChild<QCheckBox *>(QStringLiteral("sendHtml")); + second.findChild<QAbstractButton *>(QStringLiteral("sendHtml")); QVERIFY(secondToggle); QVERIFY2(secondToggle->isChecked(), "a Forward seeded from the original rather than from config"); @@ -12888,7 +13080,7 @@ void TestMainWindow::disablingInputsCoversEveryFieldAndTheToolbar() auto *to = window->findChild<QLineEdit *>(QStringLiteral("to")); auto *subject = window->findChild<QLineEdit *>(QStringLiteral("subject")); auto *from = window->findChild<QComboBox *>(QStringLiteral("from")); - auto *toggle = window->findChild<QCheckBox *>(QStringLiteral("sendHtml")); + auto *toggle = window->findChild<QAbstractButton *>(QStringLiteral("sendHtml")); QVERIFY(toolbar && to && subject && from && toggle); QVERIFY(to->isEnabled()); @@ -12914,6 +13106,41 @@ void TestMainWindow::disablingInputsCoversEveryFieldAndTheToolbar() QVERIFY2(!attachments->isEnabled(), "the attachment list is still live during a send"); + // EVERY control by name, not the toolbar that used to contain them all. + // Until item 142 split it, one setEnabled() on formatToolbar covered + // Bold through Send, so asserting on that one widget was the same as + // asserting on all of them. With the row split four ways, that assertion + // would pass while Attach sat live beside a message already being built: + // a file appended to m_attachments after MessageBuilder has run is either + // silently dropped or added to bytes already handed to the send command, + // and neither reports anything. Named individually so a control that + // grows a new home cannot quietly escape the lock. + for (const QString &name : { QStringLiteral("compose_attach"), + QStringLiteral("compose_detach"), + QStringLiteral("format_bold"), + QStringLiteral("format_italic"), + QStringLiteral("format_link"), + QStringLiteral("format_quote") }) { + auto *action = window->findChild<QAction *>(name); + QVERIFY2(action, qPrintable(QStringLiteral("no action %1").arg(name))); + QVERIFY2(!action->isEnabled(), + qPrintable(QStringLiteral("%1 is still live during a send") + .arg(name))); + } + + // The Cc/Bcc disclosure too: revealing a field mid-send is harmless on its + // own, but the fields it reveals must be as locked as the rest. + auto *ccBcc = window->findChild<QAbstractButton *>( + QStringLiteral("ccBccDisclosure")); + QVERIFY(ccBcc); + QVERIFY2(!ccBcc->isEnabled(), + "the Cc/Bcc disclosure is still live during a send"); + auto *cc = window->findChild<QLineEdit *>(QStringLiteral("cc")); + auto *bcc = window->findChild<QLineEdit *>(QStringLiteral("bcc")); + QVERIFY(cc && bcc); + QVERIFY2(!cc->isEnabled() && !bcc->isEnabled(), + "Cc or Bcc is still editable during a send"); + // /bin/true is the fixture's send command, so the send succeeds and the // composer closes itself: the message went, and holding a composer open // for a message already sent invites sending it twice. Waited on rather diff --git a/translations/qtmaildir_it_IT.ts b/translations/qtmaildir_it_IT.ts index f8a2b03..0b54957 100644 --- a/translations/qtmaildir_it_IT.ts +++ b/translations/qtmaildir_it_IT.ts @@ -8,10 +8,29 @@ <translation>Componi</translation> </message> <message> + <source>The forwarded attachments could not be extracted.</source> + <translation>Non è stato possibile estrarre gli allegati inoltrati.</translation> + </message> + <message numerus="yes"> + <source>%n forwarded attachment(s) could not be extracted: %1</source> + <translation> + <numerusform>Non è stato possibile estrarre %n allegato inoltrato: %1</numerusform> + <numerusform>Non è stato possibile estrarre %n allegati inoltrati: %1</numerusform> + </translation> + </message> + <message> <source>From:</source> <translation>Da:</translation> </message> <message> + <source>Cc/Bcc</source> + <translation>Cc/Ccn</translation> + </message> + <message> + <source>Show or hide the Cc and Bcc fields</source> + <translation>Mostra o nascondi i campi Cc e Ccn</translation> + </message> + <message> <source>To:</source> <translation>A:</translation> </message> @@ -28,14 +47,6 @@ <translation>Oggetto:</translation> </message> <message> - <source>Also send a formatted copy</source> - <translation>Invia anche una copia formattata</translation> - </message> - <message> - <source>Sends the message as plain text with a formatted version alongside it. The plain text is what you typed.</source> - <translation>Invia il messaggio come testo semplice con accanto una versione formattata. Il testo semplice è quello che hai scritto.</translation> - </message> - <message> <source>Send output</source> <translation>Output dell’invio</translation> </message> @@ -44,10 +55,6 @@ <translation>Chiudi</translation> </message> <message> - <source>Formatting</source> - <translation>Formattazione</translation> - </message> - <message> <source>Bold</source> <translation>Grassetto</translation> </message> @@ -84,6 +91,14 @@ <translation>Rimuovi allegato</translation> </message> <message> + <source>Send as HTML</source> + <translation>Invia come HTML</translation> + </message> + <message> + <source>Sends the message as plain text with an HTML version alongside it. The plain text is what you typed.</source> + <translation>Invia il messaggio come testo semplice con una versione HTML a fianco. Il testo semplice è quello che hai scritto.</translation> + </message> + <message> <source>Send</source> <translation>Invia</translation> </message> @@ -179,8 +194,8 @@ Il messaggio È stato inviato. Non inviarlo di nuovo.</translation> <translation>L'account '%1' non ha un cestino configurato; aggiungere una chiave 'trash' alla sua sezione. L'eliminazione non funzionerà per questo account finché non verrà fatto.</translation> </message> <message> - <source>[compose] quote_position '%1' is not recognised; expected above or below. Using above.</source> - <translation>[compose] quote_position '%1' non è riconosciuto; atteso above o below. Verrà usato above.</translation> + <source>[compose] quote_position '%1' is not recognised; expected above or below. Using below.</source> + <translation>[compose] quote_position '%1' non è riconosciuto; atteso above o below. Uso below.</translation> </message> <message> <source>[compose] autosave_interval_ms '%1' is not a number; using %2.</source> @@ -1098,6 +1113,10 @@ Il messaggio È stato inviato. Non inviarlo di nuovo.</translation> </translation> </message> <message> + <source>That message is no longer indexed</source> + <translation>Quel messaggio non è più indicizzato</translation> + </message> + <message> <source>(unreadable message)</source> <translation>(messaggio illeggibile)</translation> </message> |
