Merge "persistent color scheme selection"
[trojita.git] / src / Gui / ComposeWidget.cpp
blob599373f83d613a0fd3c120e295f7d16a99cccf77
1 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net>
2 Copyright (C) 2012 Peter Amidon <peter@picnicpark.org>
3 Copyright (C) 2013 - 2014 Pali Rohár <pali.rohar@gmail.com>
5 This file is part of the Trojita Qt IMAP e-mail client,
6 http://trojita.flaska.net/
8 This program is free software; you can redistribute it and/or
9 modify it under the terms of the GNU General Public License as
10 published by the Free Software Foundation; either version 2 of
11 the License or (at your option) version 3 or any later version
12 accepted by the membership of KDE e.V. (or its successor approved
13 by the membership of KDE e.V.), which shall act as a proxy
14 defined in Section 14 of version 3 of the license.
16 This program is distributed in the hope that it will be useful,
17 but WITHOUT ANY WARRANTY; without even the implied warranty of
18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 GNU General Public License for more details.
21 You should have received a copy of the GNU General Public License
22 along with this program. If not, see <http://www.gnu.org/licenses/>.
24 #include <QAbstractProxyModel>
25 #include <QBuffer>
26 #include <QDesktopWidget>
27 #include <QFileDialog>
28 #include <QGraphicsOpacityEffect>
29 #include <QKeyEvent>
30 #include <QMenu>
31 #include <QMessageBox>
32 #include <QProgressDialog>
33 #include <QPropertyAnimation>
34 #include <QRegularExpression>
35 #include <QSettings>
36 #include <QTimer>
37 #include <QToolButton>
38 #include <QUrlQuery>
40 #include "ui_ComposeWidget.h"
41 #include "Composer/ExistingMessageComposer.h"
42 #include "Composer/MessageComposer.h"
43 #include "Composer/ReplaceSignature.h"
44 #include "Composer/Mailto.h"
45 #include "Composer/SenderIdentitiesModel.h"
46 #include "Composer/Submission.h"
47 #include "Common/InvokeMethod.h"
48 #include "Common/Paths.h"
49 #include "Common/SettingsNames.h"
50 #include "Gui/CompleteMessageWidget.h"
51 #include "Gui/ComposeWidget.h"
52 #include "Gui/FromAddressProxyModel.h"
53 #include "Gui/LineEdit.h"
54 #include "Gui/MessageView.h"
55 #include "Gui/OverlayWidget.h"
56 #include "Gui/PasswordDialog.h"
57 #include "Gui/ProgressPopUp.h"
58 #include "Gui/Util.h"
59 #include "Gui/Window.h"
60 #include "Imap/Model/ImapAccess.h"
61 #include "Imap/Model/ItemRoles.h"
62 #include "Imap/Model/Model.h"
63 #include "Imap/Parser/MailAddress.h"
64 #include "Imap/Tasks/AppendTask.h"
65 #include "Imap/Tasks/GenUrlAuthTask.h"
66 #include "Imap/Tasks/UidSubmitTask.h"
67 #include "Plugins/AddressbookPlugin.h"
68 #include "Plugins/PluginManager.h"
69 #include "ShortcutHandler/ShortcutHandler.h"
70 #include "UiUtils/Color.h"
71 #include "UiUtils/IconLoader.h"
73 namespace
75 enum { OFFSET_OF_FIRST_ADDRESSEE = 1, MIN_MAX_VISIBLE_RECIPIENTS = 4 };
78 namespace Gui
81 static const QString trojita_opacityAnimation = QStringLiteral("trojita_opacityAnimation");
83 /** @short Keep track of whether the document has been updated since the last save */
84 class ComposerSaveState
86 public:
87 explicit ComposerSaveState(ComposeWidget* w)
88 : composer(w)
89 , messageUpdated(false)
90 , messageEverEdited(false)
94 void setMessageUpdated(bool updated)
96 if (updated == messageUpdated)
97 return;
98 messageUpdated = updated;
99 updateText();
103 void setMessageEverEdited(bool everEdited)
105 if (everEdited == messageEverEdited)
106 return;
107 messageEverEdited = everEdited;
108 updateText();
111 bool everEdited() {return messageEverEdited;}
112 bool updated() {return messageUpdated;}
113 private:
114 ComposeWidget* composer;
115 /** @short Has it been updated since the last time we auto-saved it? */
116 bool messageUpdated;
117 /** @short Was this message ever editted by human?
119 We have to track both of these. Simply changing the sender (and hence the signature) without any text being written
120 shall not trigger automatic saving, but on the other hand changing the sender after something was already written
121 is an important change.
123 bool messageEverEdited;
124 void updateText()
126 composer->cancelButton->setText((messageUpdated || messageEverEdited) ? QWidget::tr("Cancel...") : QWidget::tr("Cancel"));
130 /** @short Ignore dirtying events while we're preparing the widget's contents
132 Under the normal course of operation, there's plenty of events (user typing some text, etc) which lead to the composer widget
133 "remembering" that the human being has made some changes, and that these changes are probably worth a prompt for saving them
134 upon a close.
136 This guard object makes sure (via RAII) that these dirtifying events are ignored during its lifetime.
138 class InhibitComposerDirtying
140 public:
141 explicit InhibitComposerDirtying(ComposeWidget *w): w(w), wasEverEdited(w->m_saveState->everEdited()), wasEverUpdated(w->m_saveState->updated()) {}
142 ~InhibitComposerDirtying()
144 w->m_saveState->setMessageEverEdited(wasEverEdited);
145 w->m_saveState->setMessageUpdated(wasEverUpdated);
147 private:
148 ComposeWidget *w;
149 bool wasEverEdited, wasEverUpdated;
152 ComposeWidget::ComposeWidget(MainWindow *mainWindow, std::shared_ptr<Composer::AbstractComposer> messageComposer, MSA::MSAFactory *msaFactory)
153 : QWidget(0, Qt::Window)
154 , ui(new Ui::ComposeWidget)
155 , m_maxVisibleRecipients(MIN_MAX_VISIBLE_RECIPIENTS)
156 , m_sentMail(false)
157 , m_explicitDraft(false)
158 , m_appendUidReceived(false)
159 , m_appendUidValidity(0)
160 , m_appendUid(0)
161 , m_genUrlAuthReceived(false)
162 , m_mainWindow(mainWindow)
163 , m_settings(mainWindow->settings())
164 , m_composer(messageComposer)
165 , m_submission(nullptr)
166 , m_completionPopup(nullptr)
167 , m_completionReceiver(nullptr)
169 setAttribute(Qt::WA_DeleteOnClose, true);
171 QIcon winIcon;
172 winIcon.addFile(QStringLiteral(":/icons/trojita-edit-big.svg"), QSize(128, 128));
173 winIcon.addFile(QStringLiteral(":/icons/trojita-edit-small.svg"), QSize(22, 22));
174 setWindowIcon(winIcon);
176 Q_ASSERT(m_mainWindow);
177 m_mainWindow->registerComposeWindow(this);
178 QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE"));
179 QString accountId = profileName.isEmpty() ? QStringLiteral("account-0") : profileName;
180 m_submission = new Composer::Submission(this, m_composer, m_mainWindow->imapModel(), msaFactory, accountId);
181 connect(m_submission, &Composer::Submission::succeeded, this, &ComposeWidget::sent);
182 connect(m_submission, &Composer::Submission::failed, this, &ComposeWidget::gotError);
183 connect(m_submission, &Composer::Submission::failed, this, [this](const QString& message) {
184 emit logged(Common::LogKind::LOG_SUBMISSION, QStringLiteral("ComposeWidget"), message);
186 connect(m_submission, &Composer::Submission::logged, this, &ComposeWidget::logged);
187 connect(m_submission, &Composer::Submission::passwordRequested, this, &ComposeWidget::passwordRequested, Qt::QueuedConnection);
188 ui->setupUi(this);
190 if (interactiveComposer()) {
191 interactiveComposer()->setReportTrojitaVersions(m_settings->value(Common::SettingsNames::interopRevealVersions, true).toBool());
192 ui->attachmentsView->setComposer(interactiveComposer());
195 sendButton = ui->buttonBox->addButton(tr("Send"), QDialogButtonBox::AcceptRole);
196 sendButton->setIcon(UiUtils::loadIcon(QStringLiteral("mail-send")));
197 connect(sendButton, &QAbstractButton::clicked, this, &ComposeWidget::send);
198 cancelButton = ui->buttonBox->addButton(QDialogButtonBox::Cancel);
199 cancelButton->setIcon(UiUtils::loadIcon(QStringLiteral("dialog-cancel")));
200 connect(cancelButton, &QAbstractButton::clicked, this, &QWidget::close);
201 connect(ui->attachButton, &QAbstractButton::clicked, this, &ComposeWidget::slotAskForFileAttachment);
203 m_saveState = std::unique_ptr<ComposerSaveState>(new ComposerSaveState(this));
205 m_completionPopup = new QMenu(this);
206 m_completionPopup->installEventFilter(this);
207 connect(m_completionPopup, &QMenu::triggered, this, &ComposeWidget::completeRecipient);
209 // TODO: make this configurable?
210 m_completionCount = 8;
212 m_recipientListUpdateTimer = new QTimer(this);
213 m_recipientListUpdateTimer->setSingleShot(true);
214 m_recipientListUpdateTimer->setInterval(250);
215 connect(m_recipientListUpdateTimer, &QTimer::timeout, this, &ComposeWidget::updateRecipientList);
217 connect(ui->verticalSplitter, &QSplitter::splitterMoved, this, &ComposeWidget::calculateMaxVisibleRecipients);
218 calculateMaxVisibleRecipients();
220 connect(ui->recipientSlider, &QAbstractSlider::valueChanged, this, &ComposeWidget::scrollRecipients);
221 connect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange);
222 ui->recipientSlider->setMinimum(0);
223 ui->recipientSlider->setMaximum(0);
224 ui->recipientSlider->setVisible(false);
225 ui->envelopeWidget->installEventFilter(this);
227 m_markButton = new QToolButton(ui->buttonBox);
228 m_markButton->setPopupMode(QToolButton::MenuButtonPopup);
229 m_markButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
230 m_markAsReply = new QActionGroup(m_markButton);
231 m_markAsReply->setExclusive(true);
232 auto *asReplyMenu = new QMenu(m_markButton);
233 m_markButton->setMenu(asReplyMenu);
234 m_actionStandalone = asReplyMenu->addAction(UiUtils::loadIcon(QStringLiteral("format-justify-fill")), tr("New Thread"));
235 m_actionStandalone->setActionGroup(m_markAsReply);
236 m_actionStandalone->setCheckable(true);
237 m_actionStandalone->setToolTip(tr("This mail will be sent as a standalone message.<hr/>Change to preserve the reply hierarchy."));
238 m_actionInReplyTo = asReplyMenu->addAction(UiUtils::loadIcon(QStringLiteral("format-justify-right")), tr("Threaded"));
239 m_actionInReplyTo->setActionGroup(m_markAsReply);
240 m_actionInReplyTo->setCheckable(true);
242 // This is a "quick shortcut action". It shows the UI bits of the current option, but when the user clicks it,
243 // the *other* action is triggered.
244 m_actionToggleMarking = new QAction(m_markButton);
245 connect(m_actionToggleMarking, &QAction::triggered, this, &ComposeWidget::toggleReplyMarking);
246 m_markButton->setDefaultAction(m_actionToggleMarking);
248 // Unfortunately, there's no signal for toggled(QAction*), so we'll have to call QAction::trigger() to have this working
249 connect(m_markAsReply, &QActionGroup::triggered, this, &ComposeWidget::updateReplyMarkingAction);
250 m_actionStandalone->trigger();
252 m_replyModeButton = new QToolButton(ui->buttonBox);
253 m_replyModeButton->setPopupMode(QToolButton::InstantPopup);
254 m_replyModeButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
256 QMenu *replyModeMenu = new QMenu(m_replyModeButton);
257 m_replyModeButton->setMenu(replyModeMenu);
259 m_replyModeActions = new QActionGroup(m_replyModeButton);
260 m_replyModeActions->setExclusive(true);
262 m_actionHandPickedRecipients = new QAction(UiUtils::loadIcon(QStringLiteral("document-edit")) ,QStringLiteral("Hand Picked Recipients"), this);
263 replyModeMenu->addAction(m_actionHandPickedRecipients);
264 m_actionHandPickedRecipients->setActionGroup(m_replyModeActions);
265 m_actionHandPickedRecipients->setCheckable(true);
267 replyModeMenu->addSeparator();
269 QAction *placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_private"));
270 m_actionReplyModePrivate = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text());
271 m_actionReplyModePrivate->setActionGroup(m_replyModeActions);
272 m_actionReplyModePrivate->setCheckable(true);
274 placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_all_but_me"));
275 m_actionReplyModeAllButMe = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text());
276 m_actionReplyModeAllButMe->setActionGroup(m_replyModeActions);
277 m_actionReplyModeAllButMe->setCheckable(true);
279 placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_all"));
280 m_actionReplyModeAll = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text());
281 m_actionReplyModeAll->setActionGroup(m_replyModeActions);
282 m_actionReplyModeAll->setCheckable(true);
284 placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_list"));
285 m_actionReplyModeList = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text());
286 m_actionReplyModeList->setActionGroup(m_replyModeActions);
287 m_actionReplyModeList->setCheckable(true);
289 connect(m_replyModeActions, &QActionGroup::triggered, this, &ComposeWidget::updateReplyMode);
291 // We want to have the button aligned to the left; the only "portable" way of this is the ResetRole
292 // (thanks to TL for mentioning this, and for the Qt's doc for providing pretty pictures on different platforms)
293 ui->buttonBox->addButton(m_markButton, QDialogButtonBox::ResetRole);
294 // Using ResetRole for reasons same as with m_markButton. We want this button to be second from the left.
295 ui->buttonBox->addButton(m_replyModeButton, QDialogButtonBox::ResetRole);
297 m_markButton->hide();
298 m_replyModeButton->hide();
300 if (auto spellchecker = m_mainWindow->pluginManager()->spellchecker()) {
301 spellchecker->actOnEditor(ui->mailText);
304 connect(ui->mailText, &ComposerTextEdit::urlsAdded, this, &ComposeWidget::slotAttachFiles);
305 connect(ui->mailText, &ComposerTextEdit::sendRequest, this, &ComposeWidget::send);
306 connect(ui->mailText, &QTextEdit::textChanged, this, &ComposeWidget::setMessageUpdated);
307 connect(ui->subject, &QLineEdit::textChanged, this, &ComposeWidget::updateWindowTitle);
308 connect(ui->subject, &QLineEdit::textChanged, this, &ComposeWidget::setMessageUpdated);
309 connect(ui->subject, &QLineEdit::returnPressed, this, [=]() { ui->mailText->setFocus(); });
310 updateWindowTitle();
312 FromAddressProxyModel *proxy = new FromAddressProxyModel(this);
313 proxy->setSourceModel(m_mainWindow->senderIdentitiesModel());
314 ui->sender->setModel(proxy);
316 connect(ui->sender, static_cast<void (QComboBox::*)(const int)>(&QComboBox::currentIndexChanged), this, &ComposeWidget::slotUpdateSignature);
317 connect(ui->sender, &QComboBox::editTextChanged, this, &ComposeWidget::setMessageUpdated);
318 connect(ui->sender->lineEdit(), &QLineEdit::textChanged, this, &ComposeWidget::slotCheckAddressOfSender);
320 QTimer *autoSaveTimer = new QTimer(this);
321 connect(autoSaveTimer, &QTimer::timeout, this, &ComposeWidget::autoSaveDraft);
322 autoSaveTimer->start(30*1000);
324 // these are for the automatically saved drafts, i.e. no i18n for the dir name
325 m_autoSavePath = QString(Common::writablePath(Common::LOCATION_CACHE) + QLatin1String("Drafts/"));
326 QDir().mkpath(m_autoSavePath);
328 m_autoSavePath += QString::number(QDateTime::currentMSecsSinceEpoch()) + QLatin1String(".draft");
330 // Add a blank recipient row to start with
331 addRecipient(m_recipients.count(), interactiveComposer() ? Composer::ADDRESS_TO : Composer::ADDRESS_RESENT_TO, QString());
332 ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE, QFormLayout::FieldRole)->widget()->setFocus();
334 slotUpdateSignature();
336 // default size
337 int sz = ui->mailText->idealWidth();
338 ui->mailText->setMinimumSize(sz, 1000*sz/1618); // golden mean editor
339 adjustSize();
340 ui->mailText->setMinimumSize(0, 0);
341 resize(size().boundedTo(qApp->desktop()->availableGeometry().size()));
344 ComposeWidget::~ComposeWidget()
346 delete ui;
349 std::shared_ptr<Composer::MessageComposer> ComposeWidget::interactiveComposer()
351 return std::dynamic_pointer_cast<Composer::MessageComposer>(m_composer);
354 /** @short Throw a warning at an attempt to create a Compose Widget while the MSA is not configured */
355 ComposeWidget *ComposeWidget::warnIfMsaNotConfigured(ComposeWidget *widget, MainWindow *mainWindow)
357 if (!widget)
358 QMessageBox::critical(mainWindow, tr("Error"), tr("Please set appropriate settings for outgoing messages."));
359 return widget;
362 /** @short Find a nice position near the mid of the main window, try to not fully occlude another sibling */
363 void ComposeWidget::placeOnMainWindow()
365 QRect area = m_mainWindow->geometry();
366 QRect origin(0, 0, width(), height());
367 origin.moveTo(area.x() + (area.width() - width()) / 2,
368 area.y() + (area.height() - height()) / 2);
369 QRect target = origin;
371 QWidgetList siblings;
372 foreach(const QWidget *w, QApplication::topLevelWidgets()) {
373 if (w == this)
374 continue; // I'm not a sibling of myself
375 if (!qobject_cast<const ComposeWidget*>(w))
376 continue; // random other stuff
377 siblings << const_cast<QWidget*>(w);
379 int dx = 20, dy = 20;
380 int i = 0;
381 // look for a position where the window would not fully cover another composer
382 // (we don't want to mass open 10 composers stashing each other)
383 // if such composer blocks our desired geometry, the new desired geometry is
384 // tested at positions shifted by 20px circling around the original one.
385 // if we're already more than 100px off the center (what implies the user
386 // has > 20 composers open ...) we give up to not shift the window
387 // too far away, maybe even off-screen.
388 // Notice that it may still happen that some composers *together* stash a 3rd one
389 while (i < siblings.count()) {
390 if (target.contains(siblings.at(i)->geometry())) {
391 target = origin.translated(dx, dy);
392 if (dx < 0 && dy < 0) {
393 dx = dy = -dx + 20;
394 if (dx >= 120) // give up
395 break;
396 } else if (dx < 0 || dy < 0) {
397 dx = -dx;
398 if (dy > 0)
399 dy = -dy;
400 } else {
401 dx = -dx;
403 i = 0;
404 } else {
405 ++i;
408 setGeometry(target);
411 /** @short Create a blank composer window */
412 ComposeWidget *ComposeWidget::createBlank(MainWindow *mainWindow)
414 MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
415 if (!msaFactory)
416 return 0;
418 auto composer = std::make_shared<Composer::MessageComposer>(mainWindow->imapModel());
419 ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
420 w->placeOnMainWindow();
421 w->show();
422 return w;
425 /** @short Load a draft in composer window */
426 ComposeWidget *ComposeWidget::createDraft(MainWindow *mainWindow, const QString &path)
428 MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
429 if (!msaFactory)
430 return 0;
432 auto composer = std::make_shared<Composer::MessageComposer>(mainWindow->imapModel());
433 ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
434 w->loadDraft(path);
435 w->placeOnMainWindow();
436 w->show();
437 return w;
440 /** @short Create a composer window with data from a URL */
441 ComposeWidget *ComposeWidget::createFromUrl(MainWindow *mainWindow, const QUrl &url)
443 MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
444 if (!msaFactory)
445 return 0;
447 auto composer = std::make_shared<Composer::MessageComposer>(mainWindow->imapModel());
448 ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
449 InhibitComposerDirtying inhibitor(w);
450 QString subject;
451 QString body;
452 QList<QPair<Composer::RecipientKind,QString> > recipients;
453 QList<QByteArray> inReplyTo;
454 QList<QByteArray> references;
455 const QUrlQuery q(url);
457 if (!q.queryItemValue(QStringLiteral("X-Trojita-DisplayName")).isEmpty()) {
458 // There should be only single email address created by Imap::Message::MailAddress::asUrl()
459 Imap::Message::MailAddress addr;
460 if (Imap::Message::MailAddress::fromUrl(addr, url, QStringLiteral("mailto")))
461 recipients << qMakePair(Composer::ADDRESS_TO, addr.asPrettyString());
462 } else {
463 // This should be real RFC 6068 mailto:
464 Composer::parseRFC6068Mailto(url, subject, body, recipients, inReplyTo, references);
467 // NOTE: we need inReplyTo and references parameters without angle brackets, so remove them
468 for (int i = 0; i < inReplyTo.size(); ++i) {
469 if (inReplyTo[i].startsWith('<') && inReplyTo[i].endsWith('>')) {
470 inReplyTo[i] = inReplyTo[i].mid(1, inReplyTo[i].size()-2);
473 for (int i = 0; i < references.size(); ++i) {
474 if (references[i].startsWith('<') && references[i].endsWith('>')) {
475 references[i] = references[i].mid(1, references[i].size()-2);
479 w->setResponseData(recipients, subject, body, inReplyTo, references, QModelIndex());
480 if (!inReplyTo.isEmpty() || !references.isEmpty()) {
481 // We don't need to expose any UI here, but we want the in-reply-to and references information to be carried with this message
482 w->m_actionInReplyTo->setChecked(true);
484 w->placeOnMainWindow();
485 w->show();
486 return w;
489 /** @short Create a composer window for a reply */
490 ComposeWidget *ComposeWidget::createReply(MainWindow *mainWindow, const Composer::ReplyMode &mode, const QModelIndex &replyingToMessage,
491 const QList<QPair<Composer::RecipientKind, QString> > &recipients, const QString &subject,
492 const QString &body, const QList<QByteArray> &inReplyTo, const QList<QByteArray> &references)
494 MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
495 if (!msaFactory)
496 return 0;
498 auto composer = std::make_shared<Composer::MessageComposer>(mainWindow->imapModel());
499 ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
500 InhibitComposerDirtying inhibitor(w);
501 w->setResponseData(recipients, subject, body, inReplyTo, references, replyingToMessage);
502 bool ok = w->setReplyMode(mode);
503 if (!ok) {
504 QString err;
505 switch (mode) {
506 case Composer::REPLY_ALL:
507 case Composer::REPLY_ALL_BUT_ME:
508 // do nothing
509 break;
510 case Composer::REPLY_LIST:
511 err = tr("It doesn't look like this is a message to the mailing list. Please fill in the recipients manually.");
512 break;
513 case Composer::REPLY_PRIVATE:
514 err = tr("Trojitá was unable to safely determine the real e-mail address of the author of the message. "
515 "You might want to use the \"Reply All\" function and trim the list of addresses manually.");
516 break;
518 if (!err.isEmpty()) {
519 Gui::Util::messageBoxWarning(w, tr("Cannot Determine Recipients"), err);
522 w->placeOnMainWindow();
523 w->show();
524 return w;
527 /** @short Create a composer window for a mail-forward action */
528 ComposeWidget *ComposeWidget::createForward(MainWindow *mainWindow, const Composer::ForwardMode mode, const QModelIndex &forwardingMessage,
529 const QString &subject, const QList<QByteArray> &inReplyTo, const QList<QByteArray> &references)
531 MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
532 if (!msaFactory)
533 return 0;
535 auto composer = std::make_shared<Composer::MessageComposer>(mainWindow->imapModel());
536 ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
537 InhibitComposerDirtying inhibitor(w);
538 w->setResponseData(QList<QPair<Composer::RecipientKind, QString>>(), subject, QString(), inReplyTo, references, QModelIndex());
539 // We don't need to expose any UI here, but we want the in-reply-to and references information to be carried with this message
540 w->m_actionInReplyTo->setChecked(true);
542 // Prepare the message to be forwarded and add it to the attachments view
543 w->interactiveComposer()->prepareForwarding(forwardingMessage, mode);
545 w->placeOnMainWindow();
546 w->show();
547 return w;
550 ComposeWidget *ComposeWidget::createFromReadOnly(MainWindow *mainWindow, const QModelIndex &messageRoot,
551 const QList<QPair<Composer::RecipientKind, QString>>& recipients)
553 MSA::MSAFactory *msaFactory = mainWindow->msaFactory();
554 if (!msaFactory)
555 return 0;
557 auto composer = std::make_shared<Composer::ExistingMessageComposer>(messageRoot);
558 ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory);
560 for (int i = 0; i < recipients.size(); ++i) {
561 w->addRecipient(i, recipients[i].first, recipients[i].second);
563 w->updateRecipientList();
565 // Disable what needs to be nuked
566 w->ui->fromLabel->setText(tr("Sender"));
567 w->ui->subject->hide();
568 w->ui->subjectLabel->hide();
569 w->ui->attachmentBox->hide();
570 w->ui->mailText->hide();
571 auto subject = messageRoot.data(Imap::Mailbox::RoleMessageSubject).toString();
572 w->setWindowTitle(tr("Resend Mail: %1").arg(subject.isEmpty() ? tr("(no subject)") : subject));
574 // Show the full content of that e-mail as the "main body" within this widget
575 CompleteMessageWidget *messageWidget = new CompleteMessageWidget(w, mainWindow->settings(), mainWindow->pluginManager(), mainWindow->favoriteTagsModel());
576 messageWidget->messageView->setMessage(messageRoot);
577 messageWidget->messageView->setNetworkWatcher(qobject_cast<Imap::Mailbox::NetworkWatcher*>(mainWindow->imapAccess()->networkWatcher()));
578 messageWidget->setFocusPolicy(Qt::StrongFocus);
579 w->ui->verticalSplitter->insertWidget(1, messageWidget);
580 w->ui->verticalSplitter->setStretchFactor(1, 100);
582 QStringList warnings;
583 if (subject.isEmpty()) {
584 warnings << tr("Message has no subject");
586 if (messageRoot.data(Imap::Mailbox::RoleMessageMessageId).toByteArray().isEmpty()) {
587 warnings << tr("The Message-ID header is missing");
589 if (!messageRoot.data(Imap::Mailbox::RoleMessageDate).toDateTime().isValid()) {
590 warnings << tr("Message has no date");
592 if (messageRoot.data(Imap::Mailbox::RoleMessageFrom).toList().isEmpty()) {
593 warnings << tr("Nothing in the From field");
595 if (messageRoot.data(Imap::Mailbox::RoleMessageTo).toList().isEmpty()) {
596 warnings << tr("No recipients in the To field");
598 if (!warnings.isEmpty()) {
599 auto lbl = new QLabel(tr("<b>This message appears to be malformed, please be careful before sending it.</b>")
600 + QStringLiteral("<ul><li>") + warnings.join(QStringLiteral("</li><li>")) + QStringLiteral("</li></ul>"),
602 lbl->setStyleSheet(Gui::Util::cssWarningBorder());
603 w->ui->verticalSplitter->insertWidget(1, lbl);
606 w->placeOnMainWindow();
607 w->show();
608 return w;
611 void ComposeWidget::updateReplyMode()
613 bool replyModeSet = false;
614 if (m_actionReplyModePrivate->isChecked()) {
615 replyModeSet = setReplyMode(Composer::REPLY_PRIVATE);
616 } else if (m_actionReplyModeAllButMe->isChecked()) {
617 replyModeSet = setReplyMode(Composer::REPLY_ALL_BUT_ME);
618 } else if (m_actionReplyModeAll->isChecked()) {
619 replyModeSet = setReplyMode(Composer::REPLY_ALL);
620 } else if (m_actionReplyModeList->isChecked()) {
621 replyModeSet = setReplyMode(Composer::REPLY_LIST);
624 if (!replyModeSet) {
625 // This is for now by design going in one direction only, from enabled to disabled.
626 // The index to the message cannot become valid again, and simply marking the buttons as disabled does the trick quite neatly.
627 m_replyModeButton->setEnabled(m_actionHandPickedRecipients->isChecked());
628 markReplyModeHandpicked();
632 void ComposeWidget::markReplyModeHandpicked()
634 m_actionHandPickedRecipients->setChecked(true);
635 m_replyModeButton->setText(m_actionHandPickedRecipients->text());
636 m_replyModeButton->setIcon(m_actionHandPickedRecipients->icon());
639 void ComposeWidget::passwordRequested(const QString &user, const QString &host)
641 if (m_settings->value(Common::SettingsNames::smtpAuthReuseImapCredsKey, false).toBool()) {
642 auto password = qobject_cast<const Imap::Mailbox::Model*>(m_mainWindow->imapAccess()->imapModel())->imapPassword();
643 if (password.isNull()) {
644 // This can happen for example when we've always been offline since the last profile change,
645 // and the IMAP password is therefore not already cached in the IMAP model.
647 // FIXME: it would be nice to "just" call out to MainWindow::authenticationRequested() in that case,
648 // but there's no async callback when the password is available. Just some food for thought when
649 // that part gets refactored :), eventually...
650 askPassword(user, host);
651 } else {
652 m_submission->setPassword(password);
654 return;
657 Plugins::PasswordPlugin *password = m_mainWindow->pluginManager()->password();
658 if (!password) {
659 askPassword(user, host);
660 return;
663 Plugins::PasswordJob *job = password->requestPassword(m_submission->accountId(), QStringLiteral("smtp"));
664 if (!job) {
665 askPassword(user, host);
666 return;
669 connect(job, &Plugins::PasswordJob::passwordAvailable, m_submission, &Composer::Submission::setPassword);
670 connect(job, &Plugins::PasswordJob::error, this, &ComposeWidget::passwordError);
672 job->setAutoDelete(true);
673 job->setProperty("user", user);
674 job->setProperty("host", host);
675 job->start();
678 void ComposeWidget::passwordError()
680 Plugins::PasswordJob *job = static_cast<Plugins::PasswordJob *>(sender());
681 const QString &user = job->property("user").toString();
682 const QString &host = job->property("host").toString();
683 askPassword(user, host);
686 void ComposeWidget::askPassword(const QString &user, const QString &host)
688 auto w = Gui::PasswordDialog::getPassword(this, tr("Authentication Required"),
689 tr("<p>Please provide SMTP password for user <b>%1</b> on <b>%2</b>:</p>").arg(
690 user.toHtmlEscaped(),
691 host.toHtmlEscaped()));
692 connect(w, &Gui::PasswordDialog::gotPassword, m_submission, &Composer::Submission::setPassword);
693 connect(w, &Gui::PasswordDialog::rejected, m_submission, &Composer::Submission::cancelPassword);
696 void ComposeWidget::changeEvent(QEvent *e)
698 QWidget::changeEvent(e);
699 switch (e->type()) {
700 case QEvent::LanguageChange:
701 ui->retranslateUi(this);
702 break;
703 default:
704 break;
709 * We capture the close event and check whether there's something to save
710 * (not sent, not up-to-date or persistent autostore)
711 * The offer the user to store or omit the message or not close at all
714 void ComposeWidget::closeEvent(QCloseEvent *ce)
716 const bool noSaveRequired = m_sentMail || !m_saveState->everEdited() ||
717 (m_explicitDraft && !m_saveState->updated())
718 || !interactiveComposer(); // autosave to permanent draft and no update
720 if (!noSaveRequired) { // save is required
721 QMessageBox msgBox(this);
722 msgBox.setWindowModality(Qt::WindowModal);
723 msgBox.setWindowTitle(tr("Save Draft?"));
724 QString message(tr("The mail has not been sent.<br>Do you want to save the draft?"));
725 if (ui->attachmentsView->model()->rowCount() > 0)
726 message += tr("<br><span style=\"color:red\">Warning: Attachments are <b>not</b> saved with the draft!</span>");
727 msgBox.setText(message);
728 msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel);
729 msgBox.setDefaultButton(QMessageBox::Save);
730 int ret = msgBox.exec();
731 if (ret == QMessageBox::Save) {
732 if (m_explicitDraft) { // editing a present draft - override it
733 saveDraft(m_autoSavePath);
734 } else {
735 // Explicitly stored drafts should be saved in a location with proper i18n support, so let's make sure both main
736 // window and this code uses the same tr() calls
737 QString path(Common::writablePath(Common::LOCATION_DATA) + Gui::MainWindow::tr("Drafts"));
738 QDir().mkpath(path);
739 QString filename = ui->subject->text();
740 if (filename.isEmpty()) {
741 filename = QDateTime::currentDateTime().toString(Qt::ISODate);
743 // Some characters are best avoided in file names. This is probably not a definitive list, but the hope is that
744 // it's going to be more readable than an unformatted hash or similar stuff. The list of characters was taken
745 // from http://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words .
746 filename.replace(QRegularExpression(QLatin1String("[/\\\\:\"|<>*?]")), QStringLiteral("_"));
747 path = QFileDialog::getSaveFileName(this, tr("Save as"), path + QLatin1Char('/') + filename + QLatin1String(".draft"),
748 tr("Drafts") + QLatin1String(" (*.draft)"));
749 if (path.isEmpty()) { // cancelled save
750 ret = QMessageBox::Cancel;
751 } else {
752 m_explicitDraft = true;
753 saveDraft(path);
754 if (path != m_autoSavePath) // we can remove the temp save
755 QFile::remove(m_autoSavePath);
759 if (ret == QMessageBox::Cancel) {
760 ce->ignore(); // don't close the window
761 return;
764 if (m_sentMail || !m_explicitDraft) // is the mail has been sent or the user does not want to store it
765 QFile::remove(m_autoSavePath); // get rid of draft
766 ce->accept(); // ultimately close the window
771 bool ComposeWidget::buildMessageData()
773 // Recipients are checked at all times, including when bouncing/redirecting
774 QList<QPair<Composer::RecipientKind,Imap::Message::MailAddress> > recipients;
775 QString errorMessage;
776 if (!parseRecipients(recipients, errorMessage)) {
777 gotError(tr("Cannot parse recipients:\n%1").arg(errorMessage));
778 return false;
780 if (recipients.isEmpty()) {
781 gotError(tr("You haven't entered any recipients"));
782 return false;
784 m_composer->setRecipients(recipients);
786 // The same applies to the sender which is needed by some MSAs for origin information
787 Imap::Message::MailAddress fromAddress;
788 if (!Imap::Message::MailAddress::fromPrettyString(fromAddress, ui->sender->currentText())) {
789 gotError(tr("The From: address does not look like a valid one"));
790 return false;
792 m_composer->setFrom(fromAddress);
794 if (auto composer = interactiveComposer()) {
795 if (ui->subject->text().isEmpty()) {
796 gotError(tr("You haven't entered any subject. Cannot send such a mail, sorry."));
797 ui->subject->setFocus();
798 return false;
801 composer->setTimestamp(QDateTime::currentDateTime());
802 composer->setSubject(ui->subject->text());
804 QAbstractProxyModel *proxy = qobject_cast<QAbstractProxyModel*>(ui->sender->model());
805 Q_ASSERT(proxy);
807 if (ui->sender->findText(ui->sender->currentText()) != -1) {
808 QModelIndex proxyIndex = ui->sender->model()->index(ui->sender->currentIndex(), 0, ui->sender->rootModelIndex());
809 Q_ASSERT(proxyIndex.isValid());
810 composer->setOrganization(
811 proxy->mapToSource(proxyIndex).sibling(proxyIndex.row(), Composer::SenderIdentitiesModel::COLUMN_ORGANIZATION)
812 .data().toString());
814 composer->setText(ui->mailText->toPlainText());
816 if (m_actionInReplyTo->isChecked()) {
817 composer->setInReplyTo(m_inReplyTo);
818 composer->setReferences(m_references);
819 composer->setReplyingToMessage(m_replyingToMessage);
820 } else {
821 composer->setInReplyTo(QList<QByteArray>());
822 composer->setReferences(QList<QByteArray>());
823 composer->setReplyingToMessage(QModelIndex());
827 if (!m_composer->isReadyForSerialization()) {
828 gotError(tr("Cannot prepare this e-mail for sending: some parts are not available"));
829 return false;
832 return true;
835 void ComposeWidget::send()
837 if (interactiveComposer()) {
838 // Well, Trojita is of course rock solid and will never ever crash :), but experience has shown that every now and then,
839 // there is a subtle issue $somewhere. This means that it's probably a good idea to save the draft explicitly -- better
840 // than losing some work. It's cheap anyway.
841 saveDraft(m_autoSavePath);
844 if (!buildMessageData()) {
845 return;
848 const bool reuseImapCreds = m_settings->value(Common::SettingsNames::smtpAuthReuseImapCredsKey, false).toBool();
849 m_submission->setImapOptions(m_settings->value(Common::SettingsNames::composerSaveToImapKey, true).toBool(),
850 m_settings->value(Common::SettingsNames::composerImapSentKey, QStringLiteral("Sent")).toString(),
851 m_settings->value(Common::SettingsNames::imapHostKey).toString(),
852 m_settings->value(Common::SettingsNames::imapUserKey).toString(),
853 m_settings->value(Common::SettingsNames::msaMethodKey).toString() == Common::SettingsNames::methodImapSendmail);
854 m_submission->setSmtpOptions(m_settings->value(Common::SettingsNames::smtpUseBurlKey, false).toBool(),
855 reuseImapCreds ?
856 m_mainWindow->imapAccess()->username() :
857 m_settings->value(Common::SettingsNames::smtpUserKey).toString());
859 ProgressPopUp *progress = new ProgressPopUp();
860 OverlayWidget *overlay = new OverlayWidget(progress, this);
861 overlay->show();
862 setUiWidgetsEnabled(false);
864 connect(m_submission, &Composer::Submission::progressMin, progress, &ProgressPopUp::setMinimum);
865 connect(m_submission, &Composer::Submission::progressMax, progress, &ProgressPopUp::setMaximum);
866 connect(m_submission, &Composer::Submission::progress, progress, &ProgressPopUp::setValue);
867 connect(m_submission, &Composer::Submission::updateStatusMessage, progress, &ProgressPopUp::setLabelText);
868 connect(m_submission, &Composer::Submission::succeeded, overlay, &QObject::deleteLater);
869 connect(m_submission, &Composer::Submission::failed, overlay, &QObject::deleteLater);
871 m_submission->send();
874 void ComposeWidget::setUiWidgetsEnabled(const bool enabled)
876 ui->verticalSplitter->setEnabled(enabled);
877 ui->buttonBox->setEnabled(enabled);
880 /** @short Set private data members to get pre-filled by available parameters
882 The semantics of the @arg inReplyTo and @arg references are the same as described for the Composer::MessageComposer,
883 i.e. the data are not supposed to contain the angle bracket. If the @arg replyingToMessage is present, it will be used
884 as an index to a message which will get marked as replied to. This is needed because IMAP doesn't really support site-wide
885 search by a Message-Id (and it cannot possibly support it in general, either), and because Trojita's lazy loading and lack
886 of cross-session persistent indexes means that "mark as replied" and "extract message-id from" are effectively two separate
887 operations.
889 void ComposeWidget::setResponseData(const QList<QPair<Composer::RecipientKind, QString> > &recipients,
890 const QString &subject, const QString &body, const QList<QByteArray> &inReplyTo,
891 const QList<QByteArray> &references, const QModelIndex &replyingToMessage)
893 InhibitComposerDirtying inhibitor(this);
894 for (int i = 0; i < recipients.size(); ++i) {
895 addRecipient(i, recipients.at(i).first, recipients.at(i).second);
897 updateRecipientList();
898 ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE, QFormLayout::FieldRole)->widget()->setFocus();
899 ui->subject->setText(subject);
900 ui->mailText->setText(body);
901 m_inReplyTo = inReplyTo;
903 // Trim the References header as per RFC 5537
904 QList<QByteArray> trimmedReferences = references;
905 int referencesSize = QByteArray("References: ").size();
906 const int lineOverhead = 3; // one for the " " prefix, two for the \r\n suffix
907 Q_FOREACH(const QByteArray &item, references)
908 referencesSize += item.size() + lineOverhead;
909 // The magic numbers are from RFC 5537
910 while (referencesSize >= 998 && trimmedReferences.size() > 3) {
911 referencesSize -= trimmedReferences.takeAt(1).size() + lineOverhead;
913 m_references = trimmedReferences;
914 m_replyingToMessage = replyingToMessage;
915 if (m_replyingToMessage.isValid()) {
916 m_markButton->show();
917 m_replyModeButton->show();
918 // Got to use trigger() so that the default action of the QToolButton is updated
919 m_actionInReplyTo->setToolTip(tr("This mail will be marked as a response<hr/>%1").arg(
920 m_replyingToMessage.data(Imap::Mailbox::RoleMessageSubject).toString().toHtmlEscaped()
922 m_actionInReplyTo->trigger();
924 // Enable only those Reply Modes that are applicable to the message to be replied
925 Composer::RecipientList dummy;
926 m_actionReplyModePrivate->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_PRIVATE,
927 m_mainWindow->senderIdentitiesModel(),
928 m_replyingToMessage, dummy));
929 m_actionReplyModeAllButMe->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL_BUT_ME,
930 m_mainWindow->senderIdentitiesModel(),
931 m_replyingToMessage, dummy));
932 m_actionReplyModeAll->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL,
933 m_mainWindow->senderIdentitiesModel(),
934 m_replyingToMessage, dummy));
935 m_actionReplyModeList->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_LIST,
936 m_mainWindow->senderIdentitiesModel(),
937 m_replyingToMessage, dummy));
938 } else {
939 m_markButton->hide();
940 m_replyModeButton->hide();
941 m_actionInReplyTo->setToolTip(QString());
942 m_actionStandalone->trigger();
945 int row = -1;
946 bool ok = Composer::Util::chooseSenderIdentityForReply(m_mainWindow->senderIdentitiesModel(), replyingToMessage, row);
947 if (ok) {
948 Q_ASSERT(row >= 0 && row < m_mainWindow->senderIdentitiesModel()->rowCount());
949 ui->sender->setCurrentIndex(row);
952 slotUpdateSignature();
955 /** @short Find out what type of recipient to use for the last row */
956 Composer::RecipientKind ComposeWidget::recipientKindForNextRow(const Composer::RecipientKind kind)
958 using namespace Imap::Mailbox;
959 switch (kind) {
960 case Composer::ADDRESS_TO:
961 // Heuristic: if the last one is "to", chances are that the next one shall not be "to" as well.
962 // Cc is reasonable here.
963 return Composer::ADDRESS_CC;
964 case Composer::ADDRESS_RESENT_TO:
965 return Composer::ADDRESS_RESENT_CC;
966 case Composer::ADDRESS_CC:
967 case Composer::ADDRESS_BCC:
968 case Composer::ADDRESS_RESENT_CC:
969 case Composer::ADDRESS_RESENT_BCC:
970 // In any other case, it is probably better to just reuse the type of the last row
971 return kind;
972 case Composer::ADDRESS_FROM:
973 case Composer::ADDRESS_SENDER:
974 case Composer::ADDRESS_REPLY_TO:
975 case Composer::ADDRESS_RESENT_FROM:
976 case Composer::ADDRESS_RESENT_SENDER:
977 // shall never be used here
978 Q_ASSERT(false);
979 return kind;
981 Q_ASSERT(false);
982 return Composer::ADDRESS_TO;
985 //BEGIN QFormLayout workarounds
987 /** First issue: QFormLayout messes up rows by never removing them
988 * ----------------------------------------------------------------
989 * As a result insertRow(int pos, .) does not pick the expected row, but usually minor
990 * (if you ever removed all items of a row in this layout)
992 * Solution: we count all rows non empty rows and when we have enough, return the row suitable for
993 * QFormLayout (which is usually behind the requested one)
995 static int actualRow(QFormLayout *form, int row)
997 for (int i = 0, c = 0; i < form->rowCount(); ++i) {
998 if (c == row) {
999 return i;
1001 if (form->itemAt(i, QFormLayout::LabelRole) || form->itemAt(i, QFormLayout::FieldRole) ||
1002 form->itemAt(i, QFormLayout::SpanningRole))
1003 ++c;
1005 return form->rowCount(); // append
1008 /** Second (related) issue: QFormLayout messes the tab order
1009 * ----------------------------------------------------------
1010 * "Inserted" rows just get appended to the present ones and by this to the tab focus order
1011 * It's therefore necessary to fix this forcing setTabOrder()
1013 * Approach: traverse all rows until we have the widget that shall be inserted in tab order and
1014 * return it's predecessor
1017 static QWidget* formPredecessor(QFormLayout *form, QWidget *w)
1019 QWidget *pred = 0;
1020 QWidget *runner = 0;
1021 QLayoutItem *item = 0;
1022 for (int i = 0; i < form->rowCount(); ++i) {
1023 if ((item = form->itemAt(i, QFormLayout::LabelRole))) {
1024 runner = item->widget();
1025 if (runner == w)
1026 return pred;
1027 else if (runner)
1028 pred = runner;
1030 if ((item = form->itemAt(i, QFormLayout::FieldRole))) {
1031 runner = item->widget();
1032 if (runner == w)
1033 return pred;
1034 else if (runner)
1035 pred = runner;
1037 if ((item = form->itemAt(i, QFormLayout::SpanningRole))) {
1038 runner = item->widget();
1039 if (runner == w)
1040 return pred;
1041 else if (runner)
1042 pred = runner;
1045 return pred;
1048 //END QFormLayout workarounds
1050 void ComposeWidget::calculateMaxVisibleRecipients()
1052 const int oldMaxVisibleRecipients = m_maxVisibleRecipients;
1053 int spacing, bottom;
1054 ui->envelopeLayout->getContentsMargins(&spacing, &spacing, &spacing, &bottom);
1055 // we abuse the fact that there's always an addressee and that they all look the same
1056 QRect itemRects[2];
1057 for (int i = 0; i < 2; ++i) {
1058 if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::LabelRole)) {
1059 itemRects[i] |= li->geometry();
1061 if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::FieldRole)) {
1062 itemRects[i] |= li->geometry();
1064 if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::SpanningRole)) {
1065 itemRects[i] |= li->geometry();
1068 int itemHeight = itemRects[0].height();
1069 spacing = qMax(0, itemRects[0].top() - itemRects[1].bottom() - 1); // QFormLayout::[vertical]spacing() is useless ...
1070 int firstTop = itemRects[0].top();
1071 const int subjectHeight = ui->subject->height();
1072 const int height = ui->verticalSplitter->sizes().at(0) - // entire splitter area
1073 firstTop - // offset of first recipient
1074 (subjectHeight + spacing) - // for the subject
1075 bottom - // layout bottom padding
1076 2; // extra pixels padding to detect that the user wants to shrink
1077 if (itemHeight + spacing == 0) {
1078 m_maxVisibleRecipients = MIN_MAX_VISIBLE_RECIPIENTS;
1079 } else {
1080 m_maxVisibleRecipients = height / (itemHeight + spacing);
1082 if (m_maxVisibleRecipients < MIN_MAX_VISIBLE_RECIPIENTS)
1083 m_maxVisibleRecipients = MIN_MAX_VISIBLE_RECIPIENTS; // allow up to 4 recipients w/o need for a sliding
1084 if (oldMaxVisibleRecipients != m_maxVisibleRecipients) {
1085 const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients);
1086 int v = qRound(1.0f*(ui->recipientSlider->value()*m_maxVisibleRecipients)/oldMaxVisibleRecipients);
1087 ui->recipientSlider->setMaximum(max);
1088 ui->recipientSlider->setVisible(max > 0);
1089 scrollRecipients(qMin(qMax(0, v), max));
1093 void ComposeWidget::addRecipient(int position, Composer::RecipientKind kind, const QString &address)
1095 QComboBox *combo = new QComboBox(this);
1096 if (interactiveComposer()) {
1097 combo->addItem(tr("To"), Composer::ADDRESS_TO);
1098 combo->addItem(tr("Cc"), Composer::ADDRESS_CC);
1099 combo->addItem(tr("Bcc"), Composer::ADDRESS_BCC);
1100 } else {
1101 combo->addItem(tr("Resent-To"), Composer::ADDRESS_RESENT_TO);
1102 combo->addItem(tr("Resent-Cc"), Composer::ADDRESS_RESENT_CC);
1103 combo->addItem(tr("Resent-Bcc"), Composer::ADDRESS_RESENT_BCC);
1105 combo->setCurrentIndex(combo->findData(kind));
1106 LineEdit *edit = new LineEdit(address, this);
1107 slotCheckAddress(edit);
1108 connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::slotCheckAddressOfSender);
1109 connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::setMessageUpdated);
1110 connect(edit, &QLineEdit::textEdited, this, &ComposeWidget::completeRecipients);
1111 connect(edit, &QLineEdit::editingFinished, this, &ComposeWidget::collapseRecipients);
1112 connect(edit, &QLineEdit::textChanged, m_recipientListUpdateTimer, static_cast<void (QTimer::*)()>(&QTimer::start));
1113 connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::markReplyModeHandpicked);
1114 connect(edit, &QLineEdit::returnPressed, this, [=]() { gotoNextInputLineFrom(edit); });
1115 m_recipients.insert(position, Recipient(combo, edit));
1116 ui->envelopeWidget->setUpdatesEnabled(false);
1117 ui->envelopeLayout->insertRow(actualRow(ui->envelopeLayout, position + OFFSET_OF_FIRST_ADDRESSEE), combo, edit);
1118 setTabOrder(formPredecessor(ui->envelopeLayout, combo), combo);
1119 setTabOrder(combo, edit);
1120 const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients);
1121 ui->recipientSlider->setMaximum(max);
1122 ui->recipientSlider->setVisible(max > 0);
1123 if (ui->recipientSlider->isVisible()) {
1124 const int v = ui->recipientSlider->value();
1125 int keepInSight = ++position;
1126 for (int i = 0; i < m_recipients.count(); ++i) {
1127 if (m_recipients.at(i).first->hasFocus() || m_recipients.at(i).second->hasFocus()) {
1128 keepInSight = i;
1129 break;
1132 if (qAbs(keepInSight - position) < m_maxVisibleRecipients)
1133 ui->recipientSlider->setValue(position*max/m_recipients.count());
1134 if (v == ui->recipientSlider->value()) // force scroll update
1135 scrollRecipients(v);
1137 ui->envelopeWidget->setUpdatesEnabled(true);
1140 void ComposeWidget::slotCheckAddressOfSender()
1142 QLineEdit *edit = qobject_cast<QLineEdit*>(sender());
1143 Q_ASSERT(edit);
1144 slotCheckAddress(edit);
1147 void ComposeWidget::slotCheckAddress(QLineEdit *edit)
1149 Imap::Message::MailAddress addr;
1150 if (edit->text().isEmpty() || Imap::Message::MailAddress::fromPrettyString(addr, edit->text())) {
1151 edit->setPalette(QPalette());
1152 } else {
1153 QPalette p;
1154 p.setColor(QPalette::Base, UiUtils::tintColor(p.color(QPalette::Base), QColor(0xff, 0, 0, 0x20)));
1155 edit->setPalette(p);
1159 void ComposeWidget::removeRecipient(int pos)
1161 // removing the widgets from the layout is important
1162 // a) not doing so leaks (minor)
1163 // b) deleteLater() crosses the evenchain and so our actualRow function would be tricked
1164 QWidget *formerFocus = QApplication::focusWidget();
1165 if (!formerFocus)
1166 formerFocus = m_lastFocusedRecipient;
1168 if (pos + 1 < m_recipients.count()) {
1169 if (m_recipients.at(pos).first == formerFocus) {
1170 m_recipients.at(pos + 1).first->setFocus();
1171 formerFocus = m_recipients.at(pos + 1).first;
1172 } else if (m_recipients.at(pos).second == formerFocus) {
1173 m_recipients.at(pos + 1).second->setFocus();
1174 formerFocus = m_recipients.at(pos + 1).second;
1176 } else if (m_recipients.at(pos).first == formerFocus || m_recipients.at(pos).second == formerFocus) {
1177 formerFocus = 0;
1180 ui->envelopeLayout->removeWidget(m_recipients.at(pos).first);
1181 ui->envelopeLayout->removeWidget(m_recipients.at(pos).second);
1182 m_recipients.at(pos).first->deleteLater();
1183 m_recipients.at(pos).second->deleteLater();
1184 m_recipients.removeAt(pos);
1185 const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients);
1186 ui->recipientSlider->setMaximum(max);
1187 ui->recipientSlider->setVisible(max > 0);
1188 if (formerFocus) {
1189 // skip event loop, remove might be triggered by imminent focus loss
1190 CALL_LATER_NOARG(formerFocus, setFocus);
1194 static inline Composer::RecipientKind currentRecipient(const QComboBox *box)
1196 return Composer::RecipientKind(box->itemData(box->currentIndex()).toInt());
1199 void ComposeWidget::updateRecipientList()
1201 // we ensure there's always one empty available
1202 bool haveEmpty = false;
1203 for (int i = 0; i < m_recipients.count(); ++i) {
1204 if (m_recipients.at(i).second->text().isEmpty()) {
1205 if (haveEmpty) {
1206 removeRecipient(i);
1208 haveEmpty = true;
1211 if (!haveEmpty) {
1212 addRecipient(m_recipients.count(),
1213 !interactiveComposer() ?
1214 Composer::ADDRESS_RESENT_TO :
1216 m_recipients.isEmpty() ?
1217 Composer::ADDRESS_TO :
1218 recipientKindForNextRow(currentRecipient(m_recipients.last().first))
1220 QString());
1224 void ComposeWidget::gotoNextInputLineFrom(QWidget *w)
1226 bool wFound = false;
1227 for(Recipient recipient : m_recipients) {
1228 if (wFound) {
1229 recipient.second->setFocus();
1230 return;
1232 if (recipient.second == w)
1233 wFound = true;
1235 Q_ASSERT(wFound);
1236 ui->subject->setFocus();
1239 void ComposeWidget::handleFocusChange()
1241 // got explicit focus on other widget - don't restore former focused recipient on scrolling
1242 m_lastFocusedRecipient = QApplication::focusWidget();
1244 if (m_lastFocusedRecipient)
1245 QTimer::singleShot(150, this, SLOT(scrollToFocus())); // give user chance to notice the focus change disposition
1248 void ComposeWidget::scrollToFocus()
1250 if (!ui->recipientSlider->isVisible())
1251 return;
1253 QWidget *focus = QApplication::focusWidget();
1254 if (focus == ui->envelopeWidget)
1255 focus = m_lastFocusedRecipient;
1256 if (!focus)
1257 return;
1259 // if this is the first or last visible recipient, show one more (to hint there's more and allow tab progression)
1260 for (int i = 0, pos = 0; i < m_recipients.count(); ++i) {
1261 if (m_recipients.at(i).first->isVisible())
1262 ++pos;
1263 if (focus == m_recipients.at(i).first || focus == m_recipients.at(i).second) {
1264 if (pos > 1 && pos < m_maxVisibleRecipients) // prev & next are in sight
1265 break;
1266 if (pos == 1)
1267 ui->recipientSlider->setValue(i - 1); // scroll to prev
1268 else
1269 ui->recipientSlider->setValue(i + 2 - m_maxVisibleRecipients); // scroll to next
1270 break;
1273 if (focus == m_lastFocusedRecipient)
1274 focus->setFocus(); // in case we scrolled to m_lastFocusedRecipient
1277 void ComposeWidget::fadeIn(QWidget *w)
1279 QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect(w);
1280 w->setGraphicsEffect(effect);
1281 QPropertyAnimation *animation = new QPropertyAnimation(effect, "opacity", w);
1282 connect(animation, &QAbstractAnimation::finished, this, &ComposeWidget::slotFadeFinished);
1283 animation->setObjectName(trojita_opacityAnimation);
1284 animation->setDuration(333);
1285 animation->setStartValue(0.0);
1286 animation->setEndValue(1.0);
1287 animation->start(QAbstractAnimation::DeleteWhenStopped);
1290 void ComposeWidget::slotFadeFinished()
1292 Q_ASSERT(sender());
1293 QWidget *animatedEffectWidget = qobject_cast<QWidget*>(sender()->parent());
1294 Q_ASSERT(animatedEffectWidget);
1295 animatedEffectWidget->setGraphicsEffect(0); // deletes old one
1298 void ComposeWidget::scrollRecipients(int value)
1300 // ignore focus changes caused by "scrolling"
1301 disconnect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange);
1303 QList<QWidget*> visibleWidgets;
1304 for (int i = 0; i < m_recipients.count(); ++i) {
1305 // remove all widgets from the form because of vspacing - causes spurious padding
1307 QWidget *toCC = m_recipients.at(i).first;
1308 QWidget *lineEdit = m_recipients.at(i).second;
1309 if (!m_lastFocusedRecipient) { // apply only _once_
1310 if (toCC->hasFocus())
1311 m_lastFocusedRecipient = toCC;
1312 else if (lineEdit->hasFocus())
1313 m_lastFocusedRecipient = lineEdit;
1315 if (toCC->isVisible())
1316 visibleWidgets << toCC;
1317 if (lineEdit->isVisible())
1318 visibleWidgets << lineEdit;
1319 ui->envelopeLayout->removeWidget(toCC);
1320 ui->envelopeLayout->removeWidget(lineEdit);
1321 toCC->hide();
1322 lineEdit->hide();
1325 const int begin = qMin(m_recipients.count(), value);
1326 const int end = qMin(m_recipients.count(), value + m_maxVisibleRecipients);
1327 for (int i = begin, j = 0; i < end; ++i, ++j) {
1328 const int pos = actualRow(ui->envelopeLayout, j + OFFSET_OF_FIRST_ADDRESSEE);
1329 QWidget *toCC = m_recipients.at(i).first;
1330 QWidget *lineEdit = m_recipients.at(i).second;
1331 ui->envelopeLayout->insertRow(pos, toCC, lineEdit);
1332 if (!visibleWidgets.contains(toCC))
1333 fadeIn(toCC);
1334 visibleWidgets.removeOne(toCC);
1335 if (!visibleWidgets.contains(lineEdit))
1336 fadeIn(lineEdit);
1337 visibleWidgets.removeOne(lineEdit);
1338 toCC->show();
1339 lineEdit->show();
1340 setTabOrder(formPredecessor(ui->envelopeLayout, toCC), toCC);
1341 setTabOrder(toCC, lineEdit);
1342 if (toCC == m_lastFocusedRecipient)
1343 toCC->setFocus();
1344 else if (lineEdit == m_lastFocusedRecipient)
1345 lineEdit->setFocus();
1348 if (m_lastFocusedRecipient && !m_lastFocusedRecipient->hasFocus() && QApplication::focusWidget())
1349 ui->envelopeWidget->setFocus();
1351 Q_FOREACH (QWidget *w, visibleWidgets) {
1352 // was visible, is no longer -> stop animation so it won't conflict later ones
1353 w->setGraphicsEffect(0); // deletes old one
1354 if (QPropertyAnimation *pa = w->findChild<QPropertyAnimation*>(trojita_opacityAnimation))
1355 pa->stop();
1357 connect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange);
1360 void ComposeWidget::collapseRecipients()
1362 QLineEdit *edit = qobject_cast<QLineEdit*>(sender());
1363 Q_ASSERT(edit);
1364 if (edit->hasFocus() || !edit->text().isEmpty())
1365 return; // nothing to clean up
1367 // an empty recipient line just lost focus -> we "place it at the end", ie. simply remove it
1368 // and append a clone
1369 bool needEmpty = false;
1370 Composer::RecipientKind carriedKind = recipientKindForNextRow(interactiveComposer() ?
1371 Composer::RecipientKind::ADDRESS_TO :
1372 Composer::RecipientKind::ADDRESS_RESENT_TO);
1373 for (int i = 0; i < m_recipients.count() - 1; ++i) { // sic! on the -1, no action if it trails anyway
1374 if (m_recipients.at(i).second == edit) {
1375 carriedKind = currentRecipient(m_recipients.last().first);
1376 removeRecipient(i);
1377 needEmpty = true;
1378 break;
1381 if (needEmpty)
1382 addRecipient(m_recipients.count(), carriedKind, QString());
1385 void ComposeWidget::gotError(const QString &error)
1387 QMessageBox::critical(this, tr("Failed to Send Mail"), error);
1388 setUiWidgetsEnabled(true);
1391 void ComposeWidget::sent()
1393 // FIXME: move back to the currently selected mailbox
1395 m_sentMail = true;
1396 QTimer::singleShot(0, this, SLOT(close()));
1399 bool ComposeWidget::parseRecipients(QList<QPair<Composer::RecipientKind, Imap::Message::MailAddress> > &results, QString &errorMessage)
1401 for (int i = 0; i < m_recipients.size(); ++i) {
1402 Composer::RecipientKind kind = currentRecipient(m_recipients.at(i).first);
1404 QString text = m_recipients.at(i).second->text();
1405 if (text.isEmpty())
1406 continue;
1407 Imap::Message::MailAddress addr;
1408 bool ok = Imap::Message::MailAddress::fromPrettyString(addr, text);
1409 if (ok) {
1410 // TODO: should we *really* learn every junk entered into a recipient field?
1411 // m_mainWindow->addressBook()->learn(addr);
1412 results << qMakePair(kind, addr);
1413 } else {
1414 errorMessage = tr("Can't parse \"%1\" as an e-mail address.").arg(text);
1415 return false;
1418 return true;
1421 void ComposeWidget::completeRecipients(const QString &text)
1423 if (text.isEmpty()) {
1424 // if there's a popup close it and set back the receiver
1425 m_completionPopup->close();
1426 m_completionReceiver = 0;
1427 return; // we do not suggest "nothing"
1429 Q_ASSERT(sender());
1430 QLineEdit *toEdit = qobject_cast<QLineEdit*>(sender());
1431 Q_ASSERT(toEdit);
1433 Plugins::AddressbookJob *firstJob = m_firstCompletionRequests.take(toEdit);
1434 Plugins::AddressbookJob *secondJob = m_secondCompletionRequests.take(toEdit);
1436 // if two jobs are running, first was started before second so first should finish earlier
1437 // stop second job
1438 if (firstJob && secondJob) {
1439 disconnect(secondJob, nullptr, this, nullptr);
1440 secondJob->stop();
1441 secondJob->deleteLater();
1442 secondJob = 0;
1444 // now at most one job is running
1446 Plugins::AddressbookPlugin *addressbook = m_mainWindow->pluginManager()->addressbook();
1447 if (!addressbook || !(addressbook->features() & Plugins::AddressbookPlugin::FeatureCompletion))
1448 return;
1450 auto newJob = addressbook->requestCompletion(text, QStringList(), m_completionCount);
1452 if (!newJob)
1453 return;
1455 if (secondJob) {
1456 // if only second job is running move second to first and push new as second
1457 firstJob = secondJob;
1458 secondJob = newJob;
1459 } else if (firstJob) {
1460 // if only first job is running push new job as second
1461 secondJob = newJob;
1462 } else {
1463 // if no jobs is running push new job as first
1464 firstJob = newJob;
1467 if (firstJob)
1468 m_firstCompletionRequests.insert(toEdit, firstJob);
1470 if (secondJob)
1471 m_secondCompletionRequests.insert(toEdit, secondJob);
1473 connect(newJob, &Plugins::AddressbookCompletionJob::completionAvailable, this, &ComposeWidget::onCompletionAvailable);
1474 connect(newJob, &Plugins::AddressbookCompletionJob::error, this, &ComposeWidget::onCompletionFailed);
1476 newJob->setAutoDelete(true);
1477 newJob->start();
1480 void ComposeWidget::onCompletionFailed(Plugins::AddressbookJob::Error error)
1482 Q_UNUSED(error);
1483 onCompletionAvailable(Plugins::NameEmailList());
1486 void ComposeWidget::onCompletionAvailable(const Plugins::NameEmailList &completion)
1488 Plugins::AddressbookJob *job = qobject_cast<Plugins::AddressbookJob *>(sender());
1489 Q_ASSERT(job);
1490 QLineEdit *toEdit = m_firstCompletionRequests.key(job);
1492 if (!toEdit)
1493 toEdit = m_secondCompletionRequests.key(job);
1495 if (!toEdit)
1496 return;
1498 // jobs are removed from QMap below
1499 Plugins::AddressbookJob *firstJob = m_firstCompletionRequests.value(toEdit);
1500 Plugins::AddressbookJob *secondJob = m_secondCompletionRequests.value(toEdit);
1502 if (job == secondJob) {
1503 // second job finished before first and first was started before second
1504 // so stop first because it has old data
1505 if (firstJob) {
1506 disconnect(firstJob, nullptr, this, nullptr);
1507 firstJob->stop();
1508 firstJob->deleteLater();
1509 firstJob = nullptr;
1511 m_firstCompletionRequests.remove(toEdit);
1512 m_secondCompletionRequests.remove(toEdit);
1513 } else if (job == firstJob) {
1514 // first job finished, but if second is still running it will have new data, so do not stop it
1515 m_firstCompletionRequests.remove(toEdit);
1518 QStringList contacts;
1520 for (int i = 0; i < completion.size(); ++i) {
1521 const Plugins::NameEmail &item = completion.at(i);
1522 contacts << Imap::Message::MailAddress::fromNameAndMail(item.name, item.email).asPrettyString();
1525 if (contacts.isEmpty()) {
1526 m_completionReceiver = 0;
1527 m_completionPopup->close();
1528 } else {
1529 m_completionReceiver = toEdit;
1530 m_completionPopup->setUpdatesEnabled(false);
1531 QList<QAction *> acts = m_completionPopup->actions();
1532 Q_FOREACH(const QString &s, contacts)
1533 m_completionPopup->addAction(s)->setData(s);
1534 Q_FOREACH(QAction *act, acts) {
1535 m_completionPopup->removeAction(act);
1536 delete act;
1538 if (m_completionPopup->isHidden())
1539 m_completionPopup->popup(toEdit->mapToGlobal(QPoint(0, toEdit->height())));
1540 m_completionPopup->setUpdatesEnabled(true);
1544 void ComposeWidget::completeRecipient(QAction *act)
1546 if (act->data().toString().isEmpty())
1547 return;
1548 m_completionReceiver->setText(act->data().toString());
1549 m_completionReceiver = 0;
1550 m_completionPopup->close();
1553 bool ComposeWidget::eventFilter(QObject *o, QEvent *e)
1555 if (o == m_completionPopup) {
1556 if (!m_completionPopup->isVisible())
1557 return false;
1559 if (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease) {
1560 QKeyEvent *ke = static_cast<QKeyEvent*>(e);
1561 if (!( ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down || // Navigation
1562 ke->key() == Qt::Key_Escape || // "escape"
1563 ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) { // selection
1564 Q_ASSERT(m_completionReceiver);
1565 QCoreApplication::sendEvent(m_completionReceiver, e);
1566 return true;
1569 return false;
1572 if (o == ui->envelopeWidget) {
1573 if (e->type() == QEvent::Wheel) {
1574 int v = ui->recipientSlider->value();
1575 if (static_cast<QWheelEvent*>(e)->delta() > 0)
1576 --v;
1577 else
1578 ++v;
1579 // just QApplication::sendEvent(ui->recipientSlider, e) will cause a recursion if
1580 // ui->recipientSlider ignores the event (eg. because it would lead to an invalid value)
1581 // since ui->recipientSlider is child of ui->envelopeWidget
1582 // my guts tell me to not send events to children if it can be avoided, but its just a gut feeling
1583 ui->recipientSlider->setValue(v);
1584 e->accept();
1585 return true;
1587 if (e->type() == QEvent::KeyPress && ui->envelopeWidget->hasFocus()) {
1588 scrollToFocus();
1589 QWidget *focus = QApplication::focusWidget();
1590 if (focus && focus != ui->envelopeWidget) {
1591 int key = static_cast<QKeyEvent*>(e)->key();
1592 if (!(key == Qt::Key_Tab || key == Qt::Key_Backtab)) // those alter the focus again
1593 QApplication::sendEvent(focus, e);
1595 return true;
1597 if (e->type() == QEvent::Resize) {
1598 QResizeEvent *re = static_cast<QResizeEvent*>(e);
1599 if (re->size().height() != re->oldSize().height())
1600 calculateMaxVisibleRecipients();
1601 return false;
1603 return false;
1606 return false;
1610 void ComposeWidget::slotAskForFileAttachment()
1612 static QDir directory = QDir::home();
1613 QString fileName = QFileDialog::getOpenFileName(this, tr("Attach File..."), directory.absolutePath(), QString(), 0,
1614 QFileDialog::DontResolveSymlinks);
1615 if (!fileName.isEmpty()) {
1616 directory = QFileInfo(fileName).absoluteDir();
1617 interactiveComposer()->addFileAttachment(fileName);
1621 void ComposeWidget::slotAttachFiles(QList<QUrl> urls)
1623 foreach (const QUrl &url, urls) {
1624 if (url.isLocalFile()) {
1625 interactiveComposer()->addFileAttachment(url.path());
1630 void ComposeWidget::slotUpdateSignature()
1632 InhibitComposerDirtying inhibitor(this);
1633 QAbstractProxyModel *proxy = qobject_cast<QAbstractProxyModel*>(ui->sender->model());
1634 Q_ASSERT(proxy);
1635 QModelIndex proxyIndex = ui->sender->model()->index(ui->sender->currentIndex(), 0, ui->sender->rootModelIndex());
1637 if (!proxyIndex.isValid()) {
1638 // This happens when the settings dialog gets closed and the SenderIdentitiesModel reloads data from the on-disk cache
1639 return;
1642 QString newSignature = proxy->mapToSource(proxyIndex).sibling(proxyIndex.row(),
1643 Composer::SenderIdentitiesModel::COLUMN_SIGNATURE)
1644 .data().toString();
1646 Composer::Util::replaceSignature(ui->mailText->document(), newSignature);
1649 /** @short Massage the list of recipients so that they match the desired type of reply
1651 In case of an error, the original list of recipients is left as is.
1653 bool ComposeWidget::setReplyMode(const Composer::ReplyMode mode)
1655 if (!m_replyingToMessage.isValid())
1656 return false;
1658 // Determine the new list of recipients
1659 Composer::RecipientList list;
1660 if (!Composer::Util::replyRecipientList(mode, m_mainWindow->senderIdentitiesModel(),
1661 m_replyingToMessage, list)) {
1662 return false;
1665 while (!m_recipients.isEmpty())
1666 removeRecipient(0);
1668 Q_FOREACH(Composer::RecipientList::value_type recipient, list) {
1669 if (!recipient.second.hasUsefulDisplayName())
1670 recipient.second.name.clear();
1671 addRecipient(m_recipients.size(), recipient.first, recipient.second.asPrettyString());
1674 updateRecipientList();
1676 switch (mode) {
1677 case Composer::REPLY_PRIVATE:
1678 m_actionReplyModePrivate->setChecked(true);
1679 break;
1680 case Composer::REPLY_ALL_BUT_ME:
1681 m_actionReplyModeAllButMe->setChecked(true);
1682 break;
1683 case Composer::REPLY_ALL:
1684 m_actionReplyModeAll->setChecked(true);
1685 break;
1686 case Composer::REPLY_LIST:
1687 m_actionReplyModeList->setChecked(true);
1688 break;
1691 m_replyModeButton->setText(m_replyModeActions->checkedAction()->text());
1692 m_replyModeButton->setIcon(m_replyModeActions->checkedAction()->icon());
1694 ui->mailText->setFocus();
1696 return true;
1699 /** local draft serializaton:
1700 * Version (int)
1701 * Whether this draft was stored explicitly (bool)
1702 * The sender (QString)
1703 * Amount of recipients (int)
1704 * n * (RecipientKind ("int") + recipient (QString))
1705 * Subject (QString)
1706 * The message text (QString)
1709 void ComposeWidget::saveDraft(const QString &path)
1711 static const int trojitaDraftVersion = 3;
1712 QFile file(path);
1713 if (!file.open(QIODevice::WriteOnly))
1714 return; // TODO: error message?
1715 QDataStream stream(&file);
1716 stream.setVersion(QDataStream::Qt_4_6);
1717 stream << trojitaDraftVersion << m_explicitDraft << ui->sender->currentText();
1718 stream << m_recipients.count();
1719 for (int i = 0; i < m_recipients.count(); ++i) {
1720 stream << m_recipients.at(i).first->itemData(m_recipients.at(i).first->currentIndex()).toInt();
1721 stream << m_recipients.at(i).second->text();
1723 stream << m_composer->timestamp() << m_inReplyTo << m_references;
1724 stream << m_actionInReplyTo->isChecked();
1725 stream << ui->subject->text();
1726 stream << ui->mailText->toPlainText();
1727 // we spare attachments
1728 // a) serializing isn't an option, they could be HUUUGE
1729 // b) storing urls only works for urls
1730 // c) the data behind the url or the url validity might have changed
1731 // d) nasty part is writing mails - DnD a file into it is not a problem
1732 file.close();
1733 file.setPermissions(QFile::ReadOwner|QFile::WriteOwner);
1737 * When loading a draft we omit the present autostorage (content is replaced anyway) and make
1738 * the loaded path the autosave path, so all further automatic storage goes into the present
1739 * draft file
1742 void ComposeWidget::loadDraft(const QString &path)
1744 QFile file(path);
1745 if (!file.open(QIODevice::ReadOnly))
1746 return;
1748 if (m_autoSavePath != path) {
1749 QFile::remove(m_autoSavePath);
1750 m_autoSavePath = path;
1753 QDataStream stream(&file);
1754 stream.setVersion(QDataStream::Qt_4_6);
1755 QString string;
1756 int version, recipientCount;
1757 stream >> version;
1758 stream >> m_explicitDraft;
1759 stream >> string >> recipientCount; // sender / amount of recipients
1760 int senderIndex = ui->sender->findText(string);
1761 if (senderIndex != -1) {
1762 ui->sender->setCurrentIndex(senderIndex);
1763 } else {
1764 ui->sender->setEditText(string);
1766 for (int i = 0; i < recipientCount; ++i) {
1767 int kind;
1768 stream >> kind >> string;
1769 if (!string.isEmpty())
1770 addRecipient(i, static_cast<Composer::RecipientKind>(kind), string);
1772 if (version >= 2) {
1773 QDateTime timestamp;
1774 stream >> timestamp >> m_inReplyTo >> m_references;
1775 interactiveComposer()->setTimestamp(timestamp);
1776 if (!m_inReplyTo.isEmpty()) {
1777 m_markButton->show();
1778 // FIXME: in-reply-to's validitiy isn't the best check for showing or not showing the reply mode.
1779 // For eg: consider cases of mailto, forward, where valid in-reply-to won't mean choice of reply modes.
1780 m_replyModeButton->show();
1782 m_actionReplyModeAll->setEnabled(false);
1783 m_actionReplyModeAllButMe->setEnabled(false);
1784 m_actionReplyModeList->setEnabled(false);
1785 m_actionReplyModePrivate->setEnabled(false);
1786 markReplyModeHandpicked();
1788 // We do not have the message index at this point, but we can at least show the Message-Id here
1789 QStringList inReplyTo;
1790 Q_FOREACH(auto item, m_inReplyTo) {
1791 // There's no HTML escaping to worry about
1792 inReplyTo << QLatin1Char('<') + QString::fromUtf8(item.constData()) + QLatin1Char('>');
1794 m_actionInReplyTo->setToolTip(tr("This mail will be marked as a response<hr/>%1").arg(
1795 inReplyTo.join(tr("<br/>")).toHtmlEscaped()
1797 if (version == 2) {
1798 // it is always marked as a reply in v2
1799 m_actionInReplyTo->trigger();
1803 if (version >= 3) {
1804 bool replyChecked;
1805 stream >> replyChecked;
1806 // Got to use trigger() so that the default action of the QToolButton is updated
1807 if (replyChecked) {
1808 m_actionInReplyTo->trigger();
1809 } else {
1810 m_actionStandalone->trigger();
1813 stream >> string;
1814 ui->subject->setText(string);
1815 stream >> string;
1816 ui->mailText->setPlainText(string);
1817 m_saveState->setMessageUpdated(false); // this is now the most up-to-date one
1818 file.close();
1821 void ComposeWidget::autoSaveDraft()
1823 if (m_saveState->updated()) {
1824 m_saveState->setMessageUpdated(false);
1825 saveDraft(m_autoSavePath);
1829 void ComposeWidget::setMessageUpdated()
1831 m_saveState->setMessageUpdated(true);
1832 m_saveState->setMessageEverEdited(true);
1835 void ComposeWidget::updateWindowTitle()
1837 if (ui->subject->text().isEmpty()) {
1838 setWindowTitle(tr("Compose Mail"));
1839 } else {
1840 setWindowTitle(tr("%1 - Compose Mail").arg(ui->subject->text()));
1844 void ComposeWidget::toggleReplyMarking()
1846 (m_actionInReplyTo->isChecked() ? m_actionStandalone : m_actionInReplyTo)->trigger();
1849 void ComposeWidget::updateReplyMarkingAction()
1851 auto action = m_markAsReply->checkedAction();
1852 m_actionToggleMarking->setText(action->text());
1853 m_actionToggleMarking->setIcon(action->icon());
1854 m_actionToggleMarking->setToolTip(action->toolTip());