diff --git a/src/Gui/ComposeWidget.cpp b/src/Gui/ComposeWidget.cpp index 104d2790..9fb3192a 100644 --- a/src/Gui/ComposeWidget.cpp +++ b/src/Gui/ComposeWidget.cpp @@ -1,1860 +1,1856 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát Copyright (C) 2012 Peter Amidon Copyright (C) 2013 - 2014 Pali Rohár This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ui_ComposeWidget.h" #include "Composer/ExistingMessageComposer.h" #include "Composer/MessageComposer.h" #include "Composer/ReplaceSignature.h" #include "Composer/Mailto.h" #include "Composer/SenderIdentitiesModel.h" #include "Composer/Submission.h" #include "Common/InvokeMethod.h" #include "Common/Paths.h" #include "Common/SettingsNames.h" #include "Gui/CompleteMessageWidget.h" #include "Gui/ComposeWidget.h" #include "Gui/FromAddressProxyModel.h" #include "Gui/LineEdit.h" #include "Gui/MessageView.h" #include "Gui/OverlayWidget.h" #include "Gui/PasswordDialog.h" #include "Gui/ProgressPopUp.h" #include "Gui/Util.h" #include "Gui/Window.h" #include "Imap/Model/ImapAccess.h" #include "Imap/Model/ItemRoles.h" #include "Imap/Model/Model.h" #include "Imap/Parser/MailAddress.h" #include "Imap/Tasks/AppendTask.h" #include "Imap/Tasks/GenUrlAuthTask.h" #include "Imap/Tasks/UidSubmitTask.h" #include "Plugins/AddressbookPlugin.h" #include "Plugins/PluginManager.h" #include "ShortcutHandler/ShortcutHandler.h" #include "UiUtils/Color.h" #include "UiUtils/IconLoader.h" namespace { enum { OFFSET_OF_FIRST_ADDRESSEE = 1, MIN_MAX_VISIBLE_RECIPIENTS = 4 }; } namespace Gui { static const QString trojita_opacityAnimation = QStringLiteral("trojita_opacityAnimation"); /** @short Keep track of whether the document has been updated since the last save */ class ComposerSaveState { public: explicit ComposerSaveState(ComposeWidget* w) : composer(w) , messageUpdated(false) , messageEverEdited(false) { } void setMessageUpdated(bool updated) { if (updated == messageUpdated) return; messageUpdated = updated; updateText(); } void setMessageEverEdited(bool everEdited) { if (everEdited == messageEverEdited) return; messageEverEdited = everEdited; updateText(); } bool everEdited() {return messageEverEdited;} bool updated() {return messageUpdated;} private: ComposeWidget* composer; /** @short Has it been updated since the last time we auto-saved it? */ bool messageUpdated; /** @short Was this message ever editted by human? We have to track both of these. Simply changing the sender (and hence the signature) without any text being written shall not trigger automatic saving, but on the other hand changing the sender after something was already written is an important change. */ bool messageEverEdited; void updateText() { composer->cancelButton->setText((messageUpdated || messageEverEdited) ? QWidget::tr("Cancel...") : QWidget::tr("Cancel")); } }; /** @short Ignore dirtying events while we're preparing the widget's contents Under the normal course of operation, there's plenty of events (user typing some text, etc) which lead to the composer widget "remembering" that the human being has made some changes, and that these changes are probably worth a prompt for saving them upon a close. This guard object makes sure (via RAII) that these dirtifying events are ignored during its lifetime. */ class InhibitComposerDirtying { public: explicit InhibitComposerDirtying(ComposeWidget *w): w(w), wasEverEdited(w->m_saveState->everEdited()), wasEverUpdated(w->m_saveState->updated()) {} ~InhibitComposerDirtying() { w->m_saveState->setMessageEverEdited(wasEverEdited); w->m_saveState->setMessageUpdated(wasEverUpdated); } private: ComposeWidget *w; bool wasEverEdited, wasEverUpdated; }; ComposeWidget::ComposeWidget(MainWindow *mainWindow, std::shared_ptr messageComposer, MSA::MSAFactory *msaFactory) : QWidget(0, Qt::Window) , ui(new Ui::ComposeWidget) , m_maxVisibleRecipients(MIN_MAX_VISIBLE_RECIPIENTS) , m_sentMail(false) , m_explicitDraft(false) , m_appendUidReceived(false) , m_appendUidValidity(0) , m_appendUid(0) , m_genUrlAuthReceived(false) , m_mainWindow(mainWindow) , m_settings(mainWindow->settings()) , m_composer(messageComposer) , m_submission(nullptr) , m_completionPopup(nullptr) , m_completionReceiver(nullptr) { setAttribute(Qt::WA_DeleteOnClose, true); QIcon winIcon; winIcon.addFile(QStringLiteral(":/icons/trojita-edit-big.svg"), QSize(128, 128)); winIcon.addFile(QStringLiteral(":/icons/trojita-edit-small.svg"), QSize(22, 22)); setWindowIcon(winIcon); Q_ASSERT(m_mainWindow); m_mainWindow->registerComposeWindow(this); QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); QString accountId = profileName.isEmpty() ? QStringLiteral("account-0") : profileName; m_submission = new Composer::Submission(this, m_composer, m_mainWindow->imapModel(), msaFactory, accountId); connect(m_submission, &Composer::Submission::succeeded, this, &ComposeWidget::sent); connect(m_submission, &Composer::Submission::failed, this, &ComposeWidget::gotError); connect(m_submission, &Composer::Submission::failed, this, [this](const QString& message) { emit logged(Common::LogKind::LOG_SUBMISSION, QStringLiteral("ComposeWidget"), message); }); connect(m_submission, &Composer::Submission::logged, this, &ComposeWidget::logged); connect(m_submission, &Composer::Submission::passwordRequested, this, &ComposeWidget::passwordRequested, Qt::QueuedConnection); ui->setupUi(this); if (interactiveComposer()) { interactiveComposer()->setReportTrojitaVersions(m_settings->value(Common::SettingsNames::interopRevealVersions, true).toBool()); ui->attachmentsView->setComposer(interactiveComposer()); } sendButton = ui->buttonBox->addButton(tr("Send"), QDialogButtonBox::AcceptRole); sendButton->setIcon(UiUtils::loadIcon(QStringLiteral("mail-send"))); connect(sendButton, &QAbstractButton::clicked, this, &ComposeWidget::send); cancelButton = ui->buttonBox->addButton(QDialogButtonBox::Cancel); cancelButton->setIcon(UiUtils::loadIcon(QStringLiteral("dialog-cancel"))); connect(cancelButton, &QAbstractButton::clicked, this, &QWidget::close); connect(ui->attachButton, &QAbstractButton::clicked, this, &ComposeWidget::slotAskForFileAttachment); m_saveState = std::unique_ptr(new ComposerSaveState(this)); m_completionPopup = new QMenu(this); m_completionPopup->installEventFilter(this); connect(m_completionPopup, &QMenu::triggered, this, &ComposeWidget::completeRecipient); // TODO: make this configurable? m_completionCount = 8; m_recipientListUpdateTimer = new QTimer(this); m_recipientListUpdateTimer->setSingleShot(true); m_recipientListUpdateTimer->setInterval(250); connect(m_recipientListUpdateTimer, &QTimer::timeout, this, &ComposeWidget::updateRecipientList); connect(ui->verticalSplitter, &QSplitter::splitterMoved, this, &ComposeWidget::calculateMaxVisibleRecipients); calculateMaxVisibleRecipients(); connect(ui->recipientSlider, &QAbstractSlider::valueChanged, this, &ComposeWidget::scrollRecipients); connect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange); ui->recipientSlider->setMinimum(0); ui->recipientSlider->setMaximum(0); ui->recipientSlider->setVisible(false); ui->envelopeWidget->installEventFilter(this); m_markButton = new QToolButton(ui->buttonBox); m_markButton->setPopupMode(QToolButton::MenuButtonPopup); m_markButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); m_markAsReply = new QActionGroup(m_markButton); m_markAsReply->setExclusive(true); auto *asReplyMenu = new QMenu(m_markButton); m_markButton->setMenu(asReplyMenu); m_actionStandalone = asReplyMenu->addAction(UiUtils::loadIcon(QStringLiteral("format-justify-fill")), tr("New Thread")); m_actionStandalone->setActionGroup(m_markAsReply); m_actionStandalone->setCheckable(true); m_actionStandalone->setToolTip(tr("This mail will be sent as a standalone message.
Change to preserve the reply hierarchy.")); m_actionInReplyTo = asReplyMenu->addAction(UiUtils::loadIcon(QStringLiteral("format-justify-right")), tr("Threaded")); m_actionInReplyTo->setActionGroup(m_markAsReply); m_actionInReplyTo->setCheckable(true); // This is a "quick shortcut action". It shows the UI bits of the current option, but when the user clicks it, // the *other* action is triggered. m_actionToggleMarking = new QAction(m_markButton); connect(m_actionToggleMarking, &QAction::triggered, this, &ComposeWidget::toggleReplyMarking); m_markButton->setDefaultAction(m_actionToggleMarking); // Unfortunately, there's no signal for toggled(QAction*), so we'll have to call QAction::trigger() to have this working connect(m_markAsReply, &QActionGroup::triggered, this, &ComposeWidget::updateReplyMarkingAction); m_actionStandalone->trigger(); m_replyModeButton = new QToolButton(ui->buttonBox); m_replyModeButton->setPopupMode(QToolButton::InstantPopup); m_replyModeButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); QMenu *replyModeMenu = new QMenu(m_replyModeButton); m_replyModeButton->setMenu(replyModeMenu); m_replyModeActions = new QActionGroup(m_replyModeButton); m_replyModeActions->setExclusive(true); m_actionHandPickedRecipients = new QAction(UiUtils::loadIcon(QStringLiteral("document-edit")) ,QStringLiteral("Hand Picked Recipients"), this); replyModeMenu->addAction(m_actionHandPickedRecipients); m_actionHandPickedRecipients->setActionGroup(m_replyModeActions); m_actionHandPickedRecipients->setCheckable(true); replyModeMenu->addSeparator(); QAction *placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_private")); m_actionReplyModePrivate = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModePrivate->setActionGroup(m_replyModeActions); m_actionReplyModePrivate->setCheckable(true); placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_all_but_me")); m_actionReplyModeAllButMe = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModeAllButMe->setActionGroup(m_replyModeActions); m_actionReplyModeAllButMe->setCheckable(true); placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_all")); m_actionReplyModeAll = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModeAll->setActionGroup(m_replyModeActions); m_actionReplyModeAll->setCheckable(true); placeHolderAction = ShortcutHandler::instance()->action(QStringLiteral("action_reply_list")); m_actionReplyModeList = replyModeMenu->addAction(placeHolderAction->icon(), placeHolderAction->text()); m_actionReplyModeList->setActionGroup(m_replyModeActions); m_actionReplyModeList->setCheckable(true); connect(m_replyModeActions, &QActionGroup::triggered, this, &ComposeWidget::updateReplyMode); // We want to have the button aligned to the left; the only "portable" way of this is the ResetRole // (thanks to TL for mentioning this, and for the Qt's doc for providing pretty pictures on different platforms) ui->buttonBox->addButton(m_markButton, QDialogButtonBox::ResetRole); // Using ResetRole for reasons same as with m_markButton. We want this button to be second from the left. ui->buttonBox->addButton(m_replyModeButton, QDialogButtonBox::ResetRole); m_markButton->hide(); m_replyModeButton->hide(); if (auto spellchecker = m_mainWindow->pluginManager()->spellchecker()) { spellchecker->actOnEditor(ui->mailText); } connect(ui->mailText, &ComposerTextEdit::urlsAdded, this, &ComposeWidget::slotAttachFiles); connect(ui->mailText, &ComposerTextEdit::sendRequest, this, &ComposeWidget::send); connect(ui->mailText, &QTextEdit::textChanged, this, &ComposeWidget::setMessageUpdated); connect(ui->subject, &QLineEdit::textChanged, this, &ComposeWidget::updateWindowTitle); connect(ui->subject, &QLineEdit::textChanged, this, &ComposeWidget::setMessageUpdated); connect(ui->subject, &QLineEdit::returnPressed, this, [=]() { ui->mailText->setFocus(); }); updateWindowTitle(); FromAddressProxyModel *proxy = new FromAddressProxyModel(this); proxy->setSourceModel(m_mainWindow->senderIdentitiesModel()); ui->sender->setModel(proxy); connect(ui->sender, static_cast(&QComboBox::currentIndexChanged), this, &ComposeWidget::slotUpdateSignature); connect(ui->sender, &QComboBox::editTextChanged, this, &ComposeWidget::setMessageUpdated); connect(ui->sender->lineEdit(), &QLineEdit::textChanged, this, &ComposeWidget::slotCheckAddressOfSender); QTimer *autoSaveTimer = new QTimer(this); connect(autoSaveTimer, &QTimer::timeout, this, &ComposeWidget::autoSaveDraft); autoSaveTimer->start(30*1000); // these are for the automatically saved drafts, i.e. no i18n for the dir name m_autoSavePath = QString(Common::writablePath(Common::LOCATION_CACHE) + QLatin1String("Drafts/")); QDir().mkpath(m_autoSavePath); m_autoSavePath += QString::number(QDateTime::currentMSecsSinceEpoch()) + QLatin1String(".draft"); // Add a blank recipient row to start with addRecipient(m_recipients.count(), interactiveComposer() ? Composer::ADDRESS_TO : Composer::ADDRESS_RESENT_TO, QString()); ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE, QFormLayout::FieldRole)->widget()->setFocus(); slotUpdateSignature(); // default size int sz = ui->mailText->idealWidth(); ui->mailText->setMinimumSize(sz, 1000*sz/1618); // golden mean editor adjustSize(); ui->mailText->setMinimumSize(0, 0); resize(size().boundedTo(qApp->desktop()->availableGeometry().size())); } ComposeWidget::~ComposeWidget() { delete ui; } std::shared_ptr ComposeWidget::interactiveComposer() { return std::dynamic_pointer_cast(m_composer); } /** @short Throw a warning at an attempt to create a Compose Widget while the MSA is not configured */ ComposeWidget *ComposeWidget::warnIfMsaNotConfigured(ComposeWidget *widget, MainWindow *mainWindow) { if (!widget) QMessageBox::critical(mainWindow, tr("Error"), tr("Please set appropriate settings for outgoing messages.")); return widget; } /** @short Find a nice position near the mid of the main window, try to not fully occlude another sibling */ void ComposeWidget::placeOnMainWindow() { QRect area = m_mainWindow->geometry(); QRect origin(0, 0, width(), height()); origin.moveTo(area.x() + (area.width() - width()) / 2, area.y() + (area.height() - height()) / 2); QRect target = origin; QWidgetList siblings; foreach(const QWidget *w, QApplication::topLevelWidgets()) { if (w == this) continue; // I'm not a sibling of myself if (!qobject_cast(w)) continue; // random other stuff siblings << const_cast(w); } int dx = 20, dy = 20; int i = 0; // look for a position where the window would not fully cover another composer // (we don't want to mass open 10 composers stashing each other) // if such composer blocks our desired geometry, the new desired geometry is // tested at positions shifted by 20px circling around the original one. // if we're already more than 100px off the center (what implies the user // has > 20 composers open ...) we give up to not shift the window // too far away, maybe even off-screen. // Notice that it may still happen that some composers *together* stash a 3rd one while (i < siblings.count()) { if (target.contains(siblings.at(i)->geometry())) { target = origin.translated(dx, dy); if (dx < 0 && dy < 0) { dx = dy = -dx + 20; if (dx >= 120) // give up break; } else if (dx < 0 || dy < 0) { dx = -dx; if (dy > 0) dy = -dy; } else { dx = -dx; } i = 0; } else { ++i; } } setGeometry(target); } /** @short Create a blank composer window */ ComposeWidget *ComposeWidget::createBlank(MainWindow *mainWindow) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); w->placeOnMainWindow(); w->show(); return w; } /** @short Load a draft in composer window */ ComposeWidget *ComposeWidget::createDraft(MainWindow *mainWindow, const QString &path) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); w->loadDraft(path); w->placeOnMainWindow(); w->show(); return w; } /** @short Create a composer window with data from a URL */ ComposeWidget *ComposeWidget::createFromUrl(MainWindow *mainWindow, const QUrl &url) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); InhibitComposerDirtying inhibitor(w); QString subject; QString body; QList > recipients; QList inReplyTo; QList references; const QUrlQuery q(url); if (!q.queryItemValue(QStringLiteral("X-Trojita-DisplayName")).isEmpty()) { // There should be only single email address created by Imap::Message::MailAddress::asUrl() Imap::Message::MailAddress addr; if (Imap::Message::MailAddress::fromUrl(addr, url, QStringLiteral("mailto"))) recipients << qMakePair(Composer::ADDRESS_TO, addr.asPrettyString()); } else { // This should be real RFC 6068 mailto: Composer::parseRFC6068Mailto(url, subject, body, recipients, inReplyTo, references); } // NOTE: we need inReplyTo and references parameters without angle brackets, so remove them for (int i = 0; i < inReplyTo.size(); ++i) { if (inReplyTo[i].startsWith('<') && inReplyTo[i].endsWith('>')) { inReplyTo[i] = inReplyTo[i].mid(1, inReplyTo[i].size()-2); } } for (int i = 0; i < references.size(); ++i) { if (references[i].startsWith('<') && references[i].endsWith('>')) { references[i] = references[i].mid(1, references[i].size()-2); } } w->setResponseData(recipients, subject, body, inReplyTo, references, QModelIndex()); if (!inReplyTo.isEmpty() || !references.isEmpty()) { // 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 w->m_actionInReplyTo->setChecked(true); } w->placeOnMainWindow(); w->show(); return w; } /** @short Create a composer window for a reply */ ComposeWidget *ComposeWidget::createReply(MainWindow *mainWindow, const Composer::ReplyMode &mode, const QModelIndex &replyingToMessage, const QList > &recipients, const QString &subject, const QString &body, const QList &inReplyTo, const QList &references) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); InhibitComposerDirtying inhibitor(w); w->setResponseData(recipients, subject, body, inReplyTo, references, replyingToMessage); bool ok = w->setReplyMode(mode); if (!ok) { QString err; switch (mode) { case Composer::REPLY_ALL: case Composer::REPLY_ALL_BUT_ME: // do nothing break; case Composer::REPLY_LIST: err = tr("It doesn't look like this is a message to the mailing list. Please fill in the recipients manually."); break; case Composer::REPLY_PRIVATE: err = trUtf8("Trojitá was unable to safely determine the real e-mail address of the author of the message. " "You might want to use the \"Reply All\" function and trim the list of addresses manually."); break; } if (!err.isEmpty()) { Gui::Util::messageBoxWarning(w, tr("Cannot Determine Recipients"), err); } } w->placeOnMainWindow(); w->show(); return w; } /** @short Create a composer window for a mail-forward action */ ComposeWidget *ComposeWidget::createForward(MainWindow *mainWindow, const Composer::ForwardMode mode, const QModelIndex &forwardingMessage, const QString &subject, const QList &inReplyTo, const QList &references) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(mainWindow->imapModel()); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); InhibitComposerDirtying inhibitor(w); w->setResponseData(QList>(), subject, QString(), inReplyTo, references, QModelIndex()); // 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 w->m_actionInReplyTo->setChecked(true); // Prepare the message to be forwarded and add it to the attachments view w->interactiveComposer()->prepareForwarding(forwardingMessage, mode); w->placeOnMainWindow(); w->show(); return w; } ComposeWidget *ComposeWidget::createFromReadOnly(MainWindow *mainWindow, const QModelIndex &messageRoot, const QList>& recipients) { MSA::MSAFactory *msaFactory = mainWindow->msaFactory(); if (!msaFactory) return 0; auto composer = std::make_shared(messageRoot); ComposeWidget *w = new ComposeWidget(mainWindow, composer, msaFactory); for (int i = 0; i < recipients.size(); ++i) { w->addRecipient(i, recipients[i].first, recipients[i].second); } w->updateRecipientList(); // Disable what needs to be nuked w->ui->fromLabel->setText(tr("Sender")); w->ui->subject->hide(); w->ui->subjectLabel->hide(); w->ui->attachmentBox->hide(); w->ui->mailText->hide(); auto subject = messageRoot.data(Imap::Mailbox::RoleMessageSubject).toString(); w->setWindowTitle(tr("Resend Mail: %1").arg(subject.isEmpty() ? tr("(no subject)") : subject)); // Show the full content of that e-mail as the "main body" within this widget CompleteMessageWidget *messageWidget = new CompleteMessageWidget(w, mainWindow->settings(), mainWindow->pluginManager(), mainWindow->favoriteTagsModel()); messageWidget->messageView->setMessage(messageRoot); messageWidget->messageView->setNetworkWatcher(qobject_cast(mainWindow->imapAccess()->networkWatcher())); messageWidget->setFocusPolicy(Qt::StrongFocus); w->ui->verticalSplitter->insertWidget(1, messageWidget); w->ui->verticalSplitter->setStretchFactor(1, 100); QStringList warnings; if (subject.isEmpty()) { warnings << tr("Message has no subject"); } if (messageRoot.data(Imap::Mailbox::RoleMessageMessageId).toByteArray().isEmpty()) { warnings << tr("The Message-ID header is missing"); } if (!messageRoot.data(Imap::Mailbox::RoleMessageDate).toDateTime().isValid()) { warnings << tr("Message has no date"); } if (messageRoot.data(Imap::Mailbox::RoleMessageFrom).toList().isEmpty()) { warnings << tr("Nothing in the From field"); } if (messageRoot.data(Imap::Mailbox::RoleMessageTo).toList().isEmpty()) { warnings << tr("No recipients in the To field"); } if (!warnings.isEmpty()) { auto lbl = new QLabel(tr("This message appears to be malformed, please be careful before sending it.") + QStringLiteral("
  • ") + warnings.join(QStringLiteral("
  • ")) + QStringLiteral("
"), w); lbl->setStyleSheet(Gui::Util::cssWarningBorder()); w->ui->verticalSplitter->insertWidget(1, lbl); } w->placeOnMainWindow(); w->show(); return w; } void ComposeWidget::updateReplyMode() { bool replyModeSet = false; if (m_actionReplyModePrivate->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_PRIVATE); } else if (m_actionReplyModeAllButMe->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_ALL_BUT_ME); } else if (m_actionReplyModeAll->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_ALL); } else if (m_actionReplyModeList->isChecked()) { replyModeSet = setReplyMode(Composer::REPLY_LIST); } if (!replyModeSet) { // This is for now by design going in one direction only, from enabled to disabled. // The index to the message cannot become valid again, and simply marking the buttons as disabled does the trick quite neatly. m_replyModeButton->setEnabled(m_actionHandPickedRecipients->isChecked()); markReplyModeHandpicked(); } } void ComposeWidget::markReplyModeHandpicked() { m_actionHandPickedRecipients->setChecked(true); m_replyModeButton->setText(m_actionHandPickedRecipients->text()); m_replyModeButton->setIcon(m_actionHandPickedRecipients->icon()); } void ComposeWidget::passwordRequested(const QString &user, const QString &host) { if (m_settings->value(Common::SettingsNames::smtpAuthReuseImapCredsKey, false).toBool()) { auto password = qobject_cast(m_mainWindow->imapAccess()->imapModel())->imapPassword(); if (password.isNull()) { // This can happen for example when we've always been offline since the last profile change, // and the IMAP password is therefore not already cached in the IMAP model. // FIXME: it would be nice to "just" call out to MainWindow::authenticationRequested() in that case, // but there's no async callback when the password is available. Just some food for thought when // that part gets refactored :), eventually... askPassword(user, host); } else { m_submission->setPassword(password); } return; } Plugins::PasswordPlugin *password = m_mainWindow->pluginManager()->password(); if (!password) { askPassword(user, host); return; } - // FIXME: use another account-id at some point in future - // we are now using the profile to avoid overwriting passwords of - // other profiles in secure storage - // 'account-0' is the hardcoded value when not using a profile Plugins::PasswordJob *job = password->requestPassword(m_submission->accountId(), QStringLiteral("smtp")); if (!job) { askPassword(user, host); return; } connect(job, &Plugins::PasswordJob::passwordAvailable, m_submission, &Composer::Submission::setPassword); connect(job, &Plugins::PasswordJob::error, this, &ComposeWidget::passwordError); job->setAutoDelete(true); job->setProperty("user", user); job->setProperty("host", host); job->start(); } void ComposeWidget::passwordError() { Plugins::PasswordJob *job = static_cast(sender()); const QString &user = job->property("user").toString(); const QString &host = job->property("host").toString(); askPassword(user, host); } void ComposeWidget::askPassword(const QString &user, const QString &host) { auto w = Gui::PasswordDialog::getPassword(this, tr("Authentication Required"), tr("

Please provide SMTP password for user %1 on %2:

").arg( user.toHtmlEscaped(), host.toHtmlEscaped())); connect(w, &Gui::PasswordDialog::gotPassword, m_submission, &Composer::Submission::setPassword); connect(w, &Gui::PasswordDialog::rejected, m_submission, &Composer::Submission::cancelPassword); } void ComposeWidget::changeEvent(QEvent *e) { QWidget::changeEvent(e); switch (e->type()) { case QEvent::LanguageChange: ui->retranslateUi(this); break; default: break; } } /** * We capture the close event and check whether there's something to save * (not sent, not up-to-date or persistent autostore) * The offer the user to store or omit the message or not close at all */ void ComposeWidget::closeEvent(QCloseEvent *ce) { const bool noSaveRequired = m_sentMail || !m_saveState->everEdited() || (m_explicitDraft && !m_saveState->updated()) || !interactiveComposer(); // autosave to permanent draft and no update if (!noSaveRequired) { // save is required QMessageBox msgBox(this); msgBox.setWindowModality(Qt::WindowModal); msgBox.setWindowTitle(tr("Save Draft?")); QString message(tr("The mail has not been sent.
Do you want to save the draft?")); if (ui->attachmentsView->model()->rowCount() > 0) message += tr("
Warning: Attachments are not saved with the draft!"); msgBox.setText(message); msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Discard | QMessageBox::Cancel); msgBox.setDefaultButton(QMessageBox::Save); int ret = msgBox.exec(); if (ret == QMessageBox::Save) { if (m_explicitDraft) { // editing a present draft - override it saveDraft(m_autoSavePath); } else { // Explicitly stored drafts should be saved in a location with proper i18n support, so let's make sure both main // window and this code uses the same tr() calls QString path(Common::writablePath(Common::LOCATION_DATA) + Gui::MainWindow::tr("Drafts")); QDir().mkpath(path); QString filename = ui->subject->text(); if (filename.isEmpty()) { filename = QDateTime::currentDateTime().toString(Qt::ISODate); } // Some characters are best avoided in file names. This is probably not a definitive list, but the hope is that // it's going to be more readable than an unformatted hash or similar stuff. The list of characters was taken // from http://en.wikipedia.org/wiki/Filename#Reserved_characters_and_words . filename.replace(QRegularExpression(QLatin1String("[/\\\\:\"|<>*?]")), QStringLiteral("_")); path = QFileDialog::getSaveFileName(this, tr("Save as"), path + QLatin1Char('/') + filename + QLatin1String(".draft"), tr("Drafts") + QLatin1String(" (*.draft)")); if (path.isEmpty()) { // cancelled save ret = QMessageBox::Cancel; } else { m_explicitDraft = true; saveDraft(path); if (path != m_autoSavePath) // we can remove the temp save QFile::remove(m_autoSavePath); } } } if (ret == QMessageBox::Cancel) { ce->ignore(); // don't close the window return; } } if (m_sentMail || !m_explicitDraft) // is the mail has been sent or the user does not want to store it QFile::remove(m_autoSavePath); // get rid of draft ce->accept(); // ultimately close the window } bool ComposeWidget::buildMessageData() { // Recipients are checked at all times, including when bouncing/redirecting QList > recipients; QString errorMessage; if (!parseRecipients(recipients, errorMessage)) { gotError(tr("Cannot parse recipients:\n%1").arg(errorMessage)); return false; } if (recipients.isEmpty()) { gotError(tr("You haven't entered any recipients")); return false; } m_composer->setRecipients(recipients); // The same applies to the sender which is needed by some MSAs for origin information Imap::Message::MailAddress fromAddress; if (!Imap::Message::MailAddress::fromPrettyString(fromAddress, ui->sender->currentText())) { gotError(tr("The From: address does not look like a valid one")); return false; } m_composer->setFrom(fromAddress); if (auto composer = interactiveComposer()) { if (ui->subject->text().isEmpty()) { gotError(tr("You haven't entered any subject. Cannot send such a mail, sorry.")); ui->subject->setFocus(); return false; } composer->setTimestamp(QDateTime::currentDateTime()); composer->setSubject(ui->subject->text()); QAbstractProxyModel *proxy = qobject_cast(ui->sender->model()); Q_ASSERT(proxy); if (ui->sender->findText(ui->sender->currentText()) != -1) { QModelIndex proxyIndex = ui->sender->model()->index(ui->sender->currentIndex(), 0, ui->sender->rootModelIndex()); Q_ASSERT(proxyIndex.isValid()); composer->setOrganization( proxy->mapToSource(proxyIndex).sibling(proxyIndex.row(), Composer::SenderIdentitiesModel::COLUMN_ORGANIZATION) .data().toString()); } composer->setText(ui->mailText->toPlainText()); if (m_actionInReplyTo->isChecked()) { composer->setInReplyTo(m_inReplyTo); composer->setReferences(m_references); composer->setReplyingToMessage(m_replyingToMessage); } else { composer->setInReplyTo(QList()); composer->setReferences(QList()); composer->setReplyingToMessage(QModelIndex()); } } if (!m_composer->isReadyForSerialization()) { gotError(tr("Cannot prepare this e-mail for sending: some parts are not available")); return false; } return true; } void ComposeWidget::send() { if (interactiveComposer()) { // Well, Trojita is of course rock solid and will never ever crash :), but experience has shown that every now and then, // there is a subtle issue $somewhere. This means that it's probably a good idea to save the draft explicitly -- better // than losing some work. It's cheap anyway. saveDraft(m_autoSavePath); } if (!buildMessageData()) { return; } const bool reuseImapCreds = m_settings->value(Common::SettingsNames::smtpAuthReuseImapCredsKey, false).toBool(); m_submission->setImapOptions(m_settings->value(Common::SettingsNames::composerSaveToImapKey, true).toBool(), m_settings->value(Common::SettingsNames::composerImapSentKey, QStringLiteral("Sent")).toString(), m_settings->value(Common::SettingsNames::imapHostKey).toString(), m_settings->value(Common::SettingsNames::imapUserKey).toString(), m_settings->value(Common::SettingsNames::msaMethodKey).toString() == Common::SettingsNames::methodImapSendmail); m_submission->setSmtpOptions(m_settings->value(Common::SettingsNames::smtpUseBurlKey, false).toBool(), reuseImapCreds ? m_mainWindow->imapAccess()->username() : m_settings->value(Common::SettingsNames::smtpUserKey).toString()); ProgressPopUp *progress = new ProgressPopUp(); OverlayWidget *overlay = new OverlayWidget(progress, this); overlay->show(); setUiWidgetsEnabled(false); connect(m_submission, &Composer::Submission::progressMin, progress, &ProgressPopUp::setMinimum); connect(m_submission, &Composer::Submission::progressMax, progress, &ProgressPopUp::setMaximum); connect(m_submission, &Composer::Submission::progress, progress, &ProgressPopUp::setValue); connect(m_submission, &Composer::Submission::updateStatusMessage, progress, &ProgressPopUp::setLabelText); connect(m_submission, &Composer::Submission::succeeded, overlay, &QObject::deleteLater); connect(m_submission, &Composer::Submission::failed, overlay, &QObject::deleteLater); m_submission->send(); } void ComposeWidget::setUiWidgetsEnabled(const bool enabled) { ui->verticalSplitter->setEnabled(enabled); ui->buttonBox->setEnabled(enabled); } /** @short Set private data members to get pre-filled by available parameters The semantics of the @arg inReplyTo and @arg references are the same as described for the Composer::MessageComposer, i.e. the data are not supposed to contain the angle bracket. If the @arg replyingToMessage is present, it will be used as an index to a message which will get marked as replied to. This is needed because IMAP doesn't really support site-wide search by a Message-Id (and it cannot possibly support it in general, either), and because Trojita's lazy loading and lack of cross-session persistent indexes means that "mark as replied" and "extract message-id from" are effectively two separate operations. */ void ComposeWidget::setResponseData(const QList > &recipients, const QString &subject, const QString &body, const QList &inReplyTo, const QList &references, const QModelIndex &replyingToMessage) { InhibitComposerDirtying inhibitor(this); for (int i = 0; i < recipients.size(); ++i) { addRecipient(i, recipients.at(i).first, recipients.at(i).second); } updateRecipientList(); ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE, QFormLayout::FieldRole)->widget()->setFocus(); ui->subject->setText(subject); ui->mailText->setText(body); m_inReplyTo = inReplyTo; // Trim the References header as per RFC 5537 QList trimmedReferences = references; int referencesSize = QByteArray("References: ").size(); const int lineOverhead = 3; // one for the " " prefix, two for the \r\n suffix Q_FOREACH(const QByteArray &item, references) referencesSize += item.size() + lineOverhead; // The magic numbers are from RFC 5537 while (referencesSize >= 998 && trimmedReferences.size() > 3) { referencesSize -= trimmedReferences.takeAt(1).size() + lineOverhead; } m_references = trimmedReferences; m_replyingToMessage = replyingToMessage; if (m_replyingToMessage.isValid()) { m_markButton->show(); m_replyModeButton->show(); // Got to use trigger() so that the default action of the QToolButton is updated m_actionInReplyTo->setToolTip(tr("This mail will be marked as a response
%1").arg( m_replyingToMessage.data(Imap::Mailbox::RoleMessageSubject).toString().toHtmlEscaped() )); m_actionInReplyTo->trigger(); // Enable only those Reply Modes that are applicable to the message to be replied Composer::RecipientList dummy; m_actionReplyModePrivate->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_PRIVATE, m_mainWindow->senderIdentitiesModel(), m_replyingToMessage, dummy)); m_actionReplyModeAllButMe->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL_BUT_ME, m_mainWindow->senderIdentitiesModel(), m_replyingToMessage, dummy)); m_actionReplyModeAll->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL, m_mainWindow->senderIdentitiesModel(), m_replyingToMessage, dummy)); m_actionReplyModeList->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_LIST, m_mainWindow->senderIdentitiesModel(), m_replyingToMessage, dummy)); } else { m_markButton->hide(); m_replyModeButton->hide(); m_actionInReplyTo->setToolTip(QString()); m_actionStandalone->trigger(); } int row = -1; bool ok = Composer::Util::chooseSenderIdentityForReply(m_mainWindow->senderIdentitiesModel(), replyingToMessage, row); if (ok) { Q_ASSERT(row >= 0 && row < m_mainWindow->senderIdentitiesModel()->rowCount()); ui->sender->setCurrentIndex(row); } slotUpdateSignature(); } /** @short Find out what type of recipient to use for the last row */ Composer::RecipientKind ComposeWidget::recipientKindForNextRow(const Composer::RecipientKind kind) { using namespace Imap::Mailbox; switch (kind) { case Composer::ADDRESS_TO: // Heuristic: if the last one is "to", chances are that the next one shall not be "to" as well. // Cc is reasonable here. return Composer::ADDRESS_CC; case Composer::ADDRESS_RESENT_TO: return Composer::ADDRESS_RESENT_CC; case Composer::ADDRESS_CC: case Composer::ADDRESS_BCC: case Composer::ADDRESS_RESENT_CC: case Composer::ADDRESS_RESENT_BCC: // In any other case, it is probably better to just reuse the type of the last row return kind; case Composer::ADDRESS_FROM: case Composer::ADDRESS_SENDER: case Composer::ADDRESS_REPLY_TO: case Composer::ADDRESS_RESENT_FROM: case Composer::ADDRESS_RESENT_SENDER: // shall never be used here Q_ASSERT(false); return kind; } Q_ASSERT(false); return Composer::ADDRESS_TO; } //BEGIN QFormLayout workarounds /** First issue: QFormLayout messes up rows by never removing them * ---------------------------------------------------------------- * As a result insertRow(int pos, .) does not pick the expected row, but usually minor * (if you ever removed all items of a row in this layout) * * Solution: we count all rows non empty rows and when we have enough, return the row suitable for * QFormLayout (which is usually behind the requested one) */ static int actualRow(QFormLayout *form, int row) { for (int i = 0, c = 0; i < form->rowCount(); ++i) { if (c == row) { return i; } if (form->itemAt(i, QFormLayout::LabelRole) || form->itemAt(i, QFormLayout::FieldRole) || form->itemAt(i, QFormLayout::SpanningRole)) ++c; } return form->rowCount(); // append } /** Second (related) issue: QFormLayout messes the tab order * ---------------------------------------------------------- * "Inserted" rows just get appended to the present ones and by this to the tab focus order * It's therefore necessary to fix this forcing setTabOrder() * * Approach: traverse all rows until we have the widget that shall be inserted in tab order and * return it's predecessor */ static QWidget* formPredecessor(QFormLayout *form, QWidget *w) { QWidget *pred = 0; QWidget *runner = 0; QLayoutItem *item = 0; for (int i = 0; i < form->rowCount(); ++i) { if ((item = form->itemAt(i, QFormLayout::LabelRole))) { runner = item->widget(); if (runner == w) return pred; else if (runner) pred = runner; } if ((item = form->itemAt(i, QFormLayout::FieldRole))) { runner = item->widget(); if (runner == w) return pred; else if (runner) pred = runner; } if ((item = form->itemAt(i, QFormLayout::SpanningRole))) { runner = item->widget(); if (runner == w) return pred; else if (runner) pred = runner; } } return pred; } //END QFormLayout workarounds void ComposeWidget::calculateMaxVisibleRecipients() { const int oldMaxVisibleRecipients = m_maxVisibleRecipients; int spacing, bottom; ui->envelopeLayout->getContentsMargins(&spacing, &spacing, &spacing, &bottom); // we abuse the fact that there's always an addressee and that they all look the same QRect itemRects[2]; for (int i = 0; i < 2; ++i) { if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::LabelRole)) { itemRects[i] |= li->geometry(); } if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::FieldRole)) { itemRects[i] |= li->geometry(); } if (QLayoutItem *li = ui->envelopeLayout->itemAt(OFFSET_OF_FIRST_ADDRESSEE - i, QFormLayout::SpanningRole)) { itemRects[i] |= li->geometry(); } } int itemHeight = itemRects[0].height(); spacing = qMax(0, itemRects[0].top() - itemRects[1].bottom() - 1); // QFormLayout::[vertical]spacing() is useless ... int firstTop = itemRects[0].top(); const int subjectHeight = ui->subject->height(); const int height = ui->verticalSplitter->sizes().at(0) - // entire splitter area firstTop - // offset of first recipient (subjectHeight + spacing) - // for the subject bottom - // layout bottom padding 2; // extra pixels padding to detect that the user wants to shrink if (itemHeight + spacing == 0) { m_maxVisibleRecipients = MIN_MAX_VISIBLE_RECIPIENTS; } else { m_maxVisibleRecipients = height / (itemHeight + spacing); } if (m_maxVisibleRecipients < MIN_MAX_VISIBLE_RECIPIENTS) m_maxVisibleRecipients = MIN_MAX_VISIBLE_RECIPIENTS; // allow up to 4 recipients w/o need for a sliding if (oldMaxVisibleRecipients != m_maxVisibleRecipients) { const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients); int v = qRound(1.0f*(ui->recipientSlider->value()*m_maxVisibleRecipients)/oldMaxVisibleRecipients); ui->recipientSlider->setMaximum(max); ui->recipientSlider->setVisible(max > 0); scrollRecipients(qMin(qMax(0, v), max)); } } void ComposeWidget::addRecipient(int position, Composer::RecipientKind kind, const QString &address) { QComboBox *combo = new QComboBox(this); if (interactiveComposer()) { combo->addItem(tr("To"), Composer::ADDRESS_TO); combo->addItem(tr("Cc"), Composer::ADDRESS_CC); combo->addItem(tr("Bcc"), Composer::ADDRESS_BCC); } else { combo->addItem(tr("Resent-To"), Composer::ADDRESS_RESENT_TO); combo->addItem(tr("Resent-Cc"), Composer::ADDRESS_RESENT_CC); combo->addItem(tr("Resent-Bcc"), Composer::ADDRESS_RESENT_BCC); } combo->setCurrentIndex(combo->findData(kind)); LineEdit *edit = new LineEdit(address, this); slotCheckAddress(edit); connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::slotCheckAddressOfSender); connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::setMessageUpdated); connect(edit, &QLineEdit::textEdited, this, &ComposeWidget::completeRecipients); connect(edit, &QLineEdit::editingFinished, this, &ComposeWidget::collapseRecipients); connect(edit, &QLineEdit::textChanged, m_recipientListUpdateTimer, static_cast(&QTimer::start)); connect(edit, &QLineEdit::textChanged, this, &ComposeWidget::markReplyModeHandpicked); connect(edit, &QLineEdit::returnPressed, this, [=]() { gotoNextInputLineFrom(edit); }); m_recipients.insert(position, Recipient(combo, edit)); ui->envelopeWidget->setUpdatesEnabled(false); ui->envelopeLayout->insertRow(actualRow(ui->envelopeLayout, position + OFFSET_OF_FIRST_ADDRESSEE), combo, edit); setTabOrder(formPredecessor(ui->envelopeLayout, combo), combo); setTabOrder(combo, edit); const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients); ui->recipientSlider->setMaximum(max); ui->recipientSlider->setVisible(max > 0); if (ui->recipientSlider->isVisible()) { const int v = ui->recipientSlider->value(); int keepInSight = ++position; for (int i = 0; i < m_recipients.count(); ++i) { if (m_recipients.at(i).first->hasFocus() || m_recipients.at(i).second->hasFocus()) { keepInSight = i; break; } } if (qAbs(keepInSight - position) < m_maxVisibleRecipients) ui->recipientSlider->setValue(position*max/m_recipients.count()); if (v == ui->recipientSlider->value()) // force scroll update scrollRecipients(v); } ui->envelopeWidget->setUpdatesEnabled(true); } void ComposeWidget::slotCheckAddressOfSender() { QLineEdit *edit = qobject_cast(sender()); Q_ASSERT(edit); slotCheckAddress(edit); } void ComposeWidget::slotCheckAddress(QLineEdit *edit) { Imap::Message::MailAddress addr; if (edit->text().isEmpty() || Imap::Message::MailAddress::fromPrettyString(addr, edit->text())) { edit->setPalette(QPalette()); } else { QPalette p; p.setColor(QPalette::Base, UiUtils::tintColor(p.color(QPalette::Base), QColor(0xff, 0, 0, 0x20))); edit->setPalette(p); } } void ComposeWidget::removeRecipient(int pos) { // removing the widgets from the layout is important // a) not doing so leaks (minor) // b) deleteLater() crosses the evenchain and so our actualRow function would be tricked QWidget *formerFocus = QApplication::focusWidget(); if (!formerFocus) formerFocus = m_lastFocusedRecipient; if (pos + 1 < m_recipients.count()) { if (m_recipients.at(pos).first == formerFocus) { m_recipients.at(pos + 1).first->setFocus(); formerFocus = m_recipients.at(pos + 1).first; } else if (m_recipients.at(pos).second == formerFocus) { m_recipients.at(pos + 1).second->setFocus(); formerFocus = m_recipients.at(pos + 1).second; } } else if (m_recipients.at(pos).first == formerFocus || m_recipients.at(pos).second == formerFocus) { formerFocus = 0; } ui->envelopeLayout->removeWidget(m_recipients.at(pos).first); ui->envelopeLayout->removeWidget(m_recipients.at(pos).second); m_recipients.at(pos).first->deleteLater(); m_recipients.at(pos).second->deleteLater(); m_recipients.removeAt(pos); const int max = qMax(0, m_recipients.count() - m_maxVisibleRecipients); ui->recipientSlider->setMaximum(max); ui->recipientSlider->setVisible(max > 0); if (formerFocus) { // skip event loop, remove might be triggered by imminent focus loss CALL_LATER_NOARG(formerFocus, setFocus); } } static inline Composer::RecipientKind currentRecipient(const QComboBox *box) { return Composer::RecipientKind(box->itemData(box->currentIndex()).toInt()); } void ComposeWidget::updateRecipientList() { // we ensure there's always one empty available bool haveEmpty = false; for (int i = 0; i < m_recipients.count(); ++i) { if (m_recipients.at(i).second->text().isEmpty()) { if (haveEmpty) { removeRecipient(i); } haveEmpty = true; } } if (!haveEmpty) { addRecipient(m_recipients.count(), !interactiveComposer() ? Composer::ADDRESS_RESENT_TO : ( m_recipients.isEmpty() ? Composer::ADDRESS_TO : recipientKindForNextRow(currentRecipient(m_recipients.last().first)) ), QString()); } } void ComposeWidget::gotoNextInputLineFrom(QWidget *w) { bool wFound = false; for(Recipient recipient : m_recipients) { if (wFound) { recipient.second->setFocus(); return; } if (recipient.second == w) wFound = true; } Q_ASSERT(wFound); ui->subject->setFocus(); } void ComposeWidget::handleFocusChange() { // got explicit focus on other widget - don't restore former focused recipient on scrolling m_lastFocusedRecipient = QApplication::focusWidget(); if (m_lastFocusedRecipient) QTimer::singleShot(150, this, SLOT(scrollToFocus())); // give user chance to notice the focus change disposition } void ComposeWidget::scrollToFocus() { if (!ui->recipientSlider->isVisible()) return; QWidget *focus = QApplication::focusWidget(); if (focus == ui->envelopeWidget) focus = m_lastFocusedRecipient; if (!focus) return; // if this is the first or last visible recipient, show one more (to hint there's more and allow tab progression) for (int i = 0, pos = 0; i < m_recipients.count(); ++i) { if (m_recipients.at(i).first->isVisible()) ++pos; if (focus == m_recipients.at(i).first || focus == m_recipients.at(i).second) { if (pos > 1 && pos < m_maxVisibleRecipients) // prev & next are in sight break; if (pos == 1) ui->recipientSlider->setValue(i - 1); // scroll to prev else ui->recipientSlider->setValue(i + 2 - m_maxVisibleRecipients); // scroll to next break; } } if (focus == m_lastFocusedRecipient) focus->setFocus(); // in case we scrolled to m_lastFocusedRecipient } void ComposeWidget::fadeIn(QWidget *w) { QGraphicsOpacityEffect *effect = new QGraphicsOpacityEffect(w); w->setGraphicsEffect(effect); QPropertyAnimation *animation = new QPropertyAnimation(effect, "opacity", w); connect(animation, &QAbstractAnimation::finished, this, &ComposeWidget::slotFadeFinished); animation->setObjectName(trojita_opacityAnimation); animation->setDuration(333); animation->setStartValue(0.0); animation->setEndValue(1.0); animation->start(QAbstractAnimation::DeleteWhenStopped); } void ComposeWidget::slotFadeFinished() { Q_ASSERT(sender()); QWidget *animatedEffectWidget = qobject_cast(sender()->parent()); Q_ASSERT(animatedEffectWidget); animatedEffectWidget->setGraphicsEffect(0); // deletes old one } void ComposeWidget::scrollRecipients(int value) { // ignore focus changes caused by "scrolling" disconnect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange); QList visibleWidgets; for (int i = 0; i < m_recipients.count(); ++i) { // remove all widgets from the form because of vspacing - causes spurious padding QWidget *toCC = m_recipients.at(i).first; QWidget *lineEdit = m_recipients.at(i).second; if (!m_lastFocusedRecipient) { // apply only _once_ if (toCC->hasFocus()) m_lastFocusedRecipient = toCC; else if (lineEdit->hasFocus()) m_lastFocusedRecipient = lineEdit; } if (toCC->isVisible()) visibleWidgets << toCC; if (lineEdit->isVisible()) visibleWidgets << lineEdit; ui->envelopeLayout->removeWidget(toCC); ui->envelopeLayout->removeWidget(lineEdit); toCC->hide(); lineEdit->hide(); } const int begin = qMin(m_recipients.count(), value); const int end = qMin(m_recipients.count(), value + m_maxVisibleRecipients); for (int i = begin, j = 0; i < end; ++i, ++j) { const int pos = actualRow(ui->envelopeLayout, j + OFFSET_OF_FIRST_ADDRESSEE); QWidget *toCC = m_recipients.at(i).first; QWidget *lineEdit = m_recipients.at(i).second; ui->envelopeLayout->insertRow(pos, toCC, lineEdit); if (!visibleWidgets.contains(toCC)) fadeIn(toCC); visibleWidgets.removeOne(toCC); if (!visibleWidgets.contains(lineEdit)) fadeIn(lineEdit); visibleWidgets.removeOne(lineEdit); toCC->show(); lineEdit->show(); setTabOrder(formPredecessor(ui->envelopeLayout, toCC), toCC); setTabOrder(toCC, lineEdit); if (toCC == m_lastFocusedRecipient) toCC->setFocus(); else if (lineEdit == m_lastFocusedRecipient) lineEdit->setFocus(); } if (m_lastFocusedRecipient && !m_lastFocusedRecipient->hasFocus() && QApplication::focusWidget()) ui->envelopeWidget->setFocus(); Q_FOREACH (QWidget *w, visibleWidgets) { // was visible, is no longer -> stop animation so it won't conflict later ones w->setGraphicsEffect(0); // deletes old one if (QPropertyAnimation *pa = w->findChild(trojita_opacityAnimation)) pa->stop(); } connect(qApp, &QApplication::focusChanged, this, &ComposeWidget::handleFocusChange); } void ComposeWidget::collapseRecipients() { QLineEdit *edit = qobject_cast(sender()); Q_ASSERT(edit); if (edit->hasFocus() || !edit->text().isEmpty()) return; // nothing to clean up // an empty recipient line just lost focus -> we "place it at the end", ie. simply remove it // and append a clone bool needEmpty = false; Composer::RecipientKind carriedKind = recipientKindForNextRow(interactiveComposer() ? Composer::RecipientKind::ADDRESS_TO : Composer::RecipientKind::ADDRESS_RESENT_TO); for (int i = 0; i < m_recipients.count() - 1; ++i) { // sic! on the -1, no action if it trails anyway if (m_recipients.at(i).second == edit) { carriedKind = currentRecipient(m_recipients.last().first); removeRecipient(i); needEmpty = true; break; } } if (needEmpty) addRecipient(m_recipients.count(), carriedKind, QString()); } void ComposeWidget::gotError(const QString &error) { QMessageBox::critical(this, tr("Failed to Send Mail"), error); setUiWidgetsEnabled(true); } void ComposeWidget::sent() { // FIXME: move back to the currently selected mailbox m_sentMail = true; QTimer::singleShot(0, this, SLOT(close())); } bool ComposeWidget::parseRecipients(QList > &results, QString &errorMessage) { for (int i = 0; i < m_recipients.size(); ++i) { Composer::RecipientKind kind = currentRecipient(m_recipients.at(i).first); QString text = m_recipients.at(i).second->text(); if (text.isEmpty()) continue; Imap::Message::MailAddress addr; bool ok = Imap::Message::MailAddress::fromPrettyString(addr, text); if (ok) { // TODO: should we *really* learn every junk entered into a recipient field? // m_mainWindow->addressBook()->learn(addr); results << qMakePair(kind, addr); } else { errorMessage = tr("Can't parse \"%1\" as an e-mail address.").arg(text); return false; } } return true; } void ComposeWidget::completeRecipients(const QString &text) { if (text.isEmpty()) { // if there's a popup close it and set back the receiver m_completionPopup->close(); m_completionReceiver = 0; return; // we do not suggest "nothing" } Q_ASSERT(sender()); QLineEdit *toEdit = qobject_cast(sender()); Q_ASSERT(toEdit); Plugins::AddressbookJob *firstJob = m_firstCompletionRequests.take(toEdit); Plugins::AddressbookJob *secondJob = m_secondCompletionRequests.take(toEdit); // if two jobs are running, first was started before second so first should finish earlier // stop second job if (firstJob && secondJob) { disconnect(secondJob, nullptr, this, nullptr); secondJob->stop(); secondJob->deleteLater(); secondJob = 0; } // now at most one job is running Plugins::AddressbookPlugin *addressbook = m_mainWindow->pluginManager()->addressbook(); if (!addressbook || !(addressbook->features() & Plugins::AddressbookPlugin::FeatureCompletion)) return; auto newJob = addressbook->requestCompletion(text, QStringList(), m_completionCount); if (!newJob) return; if (secondJob) { // if only second job is running move second to first and push new as second firstJob = secondJob; secondJob = newJob; } else if (firstJob) { // if only first job is running push new job as second secondJob = newJob; } else { // if no jobs is running push new job as first firstJob = newJob; } if (firstJob) m_firstCompletionRequests.insert(toEdit, firstJob); if (secondJob) m_secondCompletionRequests.insert(toEdit, secondJob); connect(newJob, &Plugins::AddressbookCompletionJob::completionAvailable, this, &ComposeWidget::onCompletionAvailable); connect(newJob, &Plugins::AddressbookCompletionJob::error, this, &ComposeWidget::onCompletionFailed); newJob->setAutoDelete(true); newJob->start(); } void ComposeWidget::onCompletionFailed(Plugins::AddressbookJob::Error error) { Q_UNUSED(error); onCompletionAvailable(Plugins::NameEmailList()); } void ComposeWidget::onCompletionAvailable(const Plugins::NameEmailList &completion) { Plugins::AddressbookJob *job = qobject_cast(sender()); Q_ASSERT(job); QLineEdit *toEdit = m_firstCompletionRequests.key(job); if (!toEdit) toEdit = m_secondCompletionRequests.key(job); if (!toEdit) return; // jobs are removed from QMap below Plugins::AddressbookJob *firstJob = m_firstCompletionRequests.value(toEdit); Plugins::AddressbookJob *secondJob = m_secondCompletionRequests.value(toEdit); if (job == secondJob) { // second job finished before first and first was started before second // so stop first because it has old data if (firstJob) { disconnect(firstJob, nullptr, this, nullptr); firstJob->stop(); firstJob->deleteLater(); firstJob = nullptr; } m_firstCompletionRequests.remove(toEdit); m_secondCompletionRequests.remove(toEdit); } else if (job == firstJob) { // first job finished, but if second is still running it will have new data, so do not stop it m_firstCompletionRequests.remove(toEdit); } QStringList contacts; for (int i = 0; i < completion.size(); ++i) { const Plugins::NameEmail &item = completion.at(i); contacts << Imap::Message::MailAddress::fromNameAndMail(item.name, item.email).asPrettyString(); } if (contacts.isEmpty()) { m_completionReceiver = 0; m_completionPopup->close(); } else { m_completionReceiver = toEdit; m_completionPopup->setUpdatesEnabled(false); QList acts = m_completionPopup->actions(); Q_FOREACH(const QString &s, contacts) m_completionPopup->addAction(s)->setData(s); Q_FOREACH(QAction *act, acts) { m_completionPopup->removeAction(act); delete act; } if (m_completionPopup->isHidden()) m_completionPopup->popup(toEdit->mapToGlobal(QPoint(0, toEdit->height()))); m_completionPopup->setUpdatesEnabled(true); } } void ComposeWidget::completeRecipient(QAction *act) { if (act->data().toString().isEmpty()) return; m_completionReceiver->setText(act->data().toString()); m_completionReceiver = 0; m_completionPopup->close(); } bool ComposeWidget::eventFilter(QObject *o, QEvent *e) { if (o == m_completionPopup) { if (!m_completionPopup->isVisible()) return false; if (e->type() == QEvent::KeyPress || e->type() == QEvent::KeyRelease) { QKeyEvent *ke = static_cast(e); if (!( ke->key() == Qt::Key_Up || ke->key() == Qt::Key_Down || // Navigation ke->key() == Qt::Key_Escape || // "escape" ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter)) { // selection Q_ASSERT(m_completionReceiver); QCoreApplication::sendEvent(m_completionReceiver, e); return true; } } return false; } if (o == ui->envelopeWidget) { if (e->type() == QEvent::Wheel) { int v = ui->recipientSlider->value(); if (static_cast(e)->delta() > 0) --v; else ++v; // just QApplication::sendEvent(ui->recipientSlider, e) will cause a recursion if // ui->recipientSlider ignores the event (eg. because it would lead to an invalid value) // since ui->recipientSlider is child of ui->envelopeWidget // my guts tell me to not send events to children if it can be avoided, but its just a gut feeling ui->recipientSlider->setValue(v); e->accept(); return true; } if (e->type() == QEvent::KeyPress && ui->envelopeWidget->hasFocus()) { scrollToFocus(); QWidget *focus = QApplication::focusWidget(); if (focus && focus != ui->envelopeWidget) { int key = static_cast(e)->key(); if (!(key == Qt::Key_Tab || key == Qt::Key_Backtab)) // those alter the focus again QApplication::sendEvent(focus, e); } return true; } if (e->type() == QEvent::Resize) { QResizeEvent *re = static_cast(e); if (re->size().height() != re->oldSize().height()) calculateMaxVisibleRecipients(); return false; } return false; } return false; } void ComposeWidget::slotAskForFileAttachment() { static QDir directory = QDir::home(); QString fileName = QFileDialog::getOpenFileName(this, tr("Attach File..."), directory.absolutePath(), QString(), 0, QFileDialog::DontResolveSymlinks); if (!fileName.isEmpty()) { directory = QFileInfo(fileName).absoluteDir(); interactiveComposer()->addFileAttachment(fileName); } } void ComposeWidget::slotAttachFiles(QList urls) { foreach (const QUrl &url, urls) { if (url.isLocalFile()) { interactiveComposer()->addFileAttachment(url.path()); } } } void ComposeWidget::slotUpdateSignature() { InhibitComposerDirtying inhibitor(this); QAbstractProxyModel *proxy = qobject_cast(ui->sender->model()); Q_ASSERT(proxy); QModelIndex proxyIndex = ui->sender->model()->index(ui->sender->currentIndex(), 0, ui->sender->rootModelIndex()); if (!proxyIndex.isValid()) { // This happens when the settings dialog gets closed and the SenderIdentitiesModel reloads data from the on-disk cache return; } QString newSignature = proxy->mapToSource(proxyIndex).sibling(proxyIndex.row(), Composer::SenderIdentitiesModel::COLUMN_SIGNATURE) .data().toString(); Composer::Util::replaceSignature(ui->mailText->document(), newSignature); } /** @short Massage the list of recipients so that they match the desired type of reply In case of an error, the original list of recipients is left as is. */ bool ComposeWidget::setReplyMode(const Composer::ReplyMode mode) { if (!m_replyingToMessage.isValid()) return false; // Determine the new list of recipients Composer::RecipientList list; if (!Composer::Util::replyRecipientList(mode, m_mainWindow->senderIdentitiesModel(), m_replyingToMessage, list)) { return false; } while (!m_recipients.isEmpty()) removeRecipient(0); Q_FOREACH(Composer::RecipientList::value_type recipient, list) { if (!recipient.second.hasUsefulDisplayName()) recipient.second.name.clear(); addRecipient(m_recipients.size(), recipient.first, recipient.second.asPrettyString()); } updateRecipientList(); switch (mode) { case Composer::REPLY_PRIVATE: m_actionReplyModePrivate->setChecked(true); break; case Composer::REPLY_ALL_BUT_ME: m_actionReplyModeAllButMe->setChecked(true); break; case Composer::REPLY_ALL: m_actionReplyModeAll->setChecked(true); break; case Composer::REPLY_LIST: m_actionReplyModeList->setChecked(true); break; } m_replyModeButton->setText(m_replyModeActions->checkedAction()->text()); m_replyModeButton->setIcon(m_replyModeActions->checkedAction()->icon()); ui->mailText->setFocus(); return true; } /** local draft serializaton: * Version (int) * Whether this draft was stored explicitly (bool) * The sender (QString) * Amount of recipients (int) * n * (RecipientKind ("int") + recipient (QString)) * Subject (QString) * The message text (QString) */ void ComposeWidget::saveDraft(const QString &path) { static const int trojitaDraftVersion = 3; QFile file(path); if (!file.open(QIODevice::WriteOnly)) return; // TODO: error message? QDataStream stream(&file); stream.setVersion(QDataStream::Qt_4_6); stream << trojitaDraftVersion << m_explicitDraft << ui->sender->currentText(); stream << m_recipients.count(); for (int i = 0; i < m_recipients.count(); ++i) { stream << m_recipients.at(i).first->itemData(m_recipients.at(i).first->currentIndex()).toInt(); stream << m_recipients.at(i).second->text(); } stream << m_composer->timestamp() << m_inReplyTo << m_references; stream << m_actionInReplyTo->isChecked(); stream << ui->subject->text(); stream << ui->mailText->toPlainText(); // we spare attachments // a) serializing isn't an option, they could be HUUUGE // b) storing urls only works for urls // c) the data behind the url or the url validity might have changed // d) nasty part is writing mails - DnD a file into it is not a problem file.close(); file.setPermissions(QFile::ReadOwner|QFile::WriteOwner); } /** * When loading a draft we omit the present autostorage (content is replaced anyway) and make * the loaded path the autosave path, so all further automatic storage goes into the present * draft file */ void ComposeWidget::loadDraft(const QString &path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) return; if (m_autoSavePath != path) { QFile::remove(m_autoSavePath); m_autoSavePath = path; } QDataStream stream(&file); stream.setVersion(QDataStream::Qt_4_6); QString string; int version, recipientCount; stream >> version; stream >> m_explicitDraft; stream >> string >> recipientCount; // sender / amount of recipients int senderIndex = ui->sender->findText(string); if (senderIndex != -1) { ui->sender->setCurrentIndex(senderIndex); } else { ui->sender->setEditText(string); } for (int i = 0; i < recipientCount; ++i) { int kind; stream >> kind >> string; if (!string.isEmpty()) addRecipient(i, static_cast(kind), string); } if (version >= 2) { QDateTime timestamp; stream >> timestamp >> m_inReplyTo >> m_references; interactiveComposer()->setTimestamp(timestamp); if (!m_inReplyTo.isEmpty()) { m_markButton->show(); // FIXME: in-reply-to's validitiy isn't the best check for showing or not showing the reply mode. // For eg: consider cases of mailto, forward, where valid in-reply-to won't mean choice of reply modes. m_replyModeButton->show(); m_actionReplyModeAll->setEnabled(false); m_actionReplyModeAllButMe->setEnabled(false); m_actionReplyModeList->setEnabled(false); m_actionReplyModePrivate->setEnabled(false); markReplyModeHandpicked(); // We do not have the message index at this point, but we can at least show the Message-Id here QStringList inReplyTo; Q_FOREACH(auto item, m_inReplyTo) { // There's no HTML escaping to worry about inReplyTo << QLatin1Char('<') + QString::fromUtf8(item.constData()) + QLatin1Char('>'); } m_actionInReplyTo->setToolTip(tr("This mail will be marked as a response
%1").arg( inReplyTo.join(tr("
")).toHtmlEscaped() )); if (version == 2) { // it is always marked as a reply in v2 m_actionInReplyTo->trigger(); } } } if (version >= 3) { bool replyChecked; stream >> replyChecked; // Got to use trigger() so that the default action of the QToolButton is updated if (replyChecked) { m_actionInReplyTo->trigger(); } else { m_actionStandalone->trigger(); } } stream >> string; ui->subject->setText(string); stream >> string; ui->mailText->setPlainText(string); m_saveState->setMessageUpdated(false); // this is now the most up-to-date one file.close(); } void ComposeWidget::autoSaveDraft() { if (m_saveState->updated()) { m_saveState->setMessageUpdated(false); saveDraft(m_autoSavePath); } } void ComposeWidget::setMessageUpdated() { m_saveState->setMessageUpdated(true); m_saveState->setMessageEverEdited(true); } void ComposeWidget::updateWindowTitle() { if (ui->subject->text().isEmpty()) { setWindowTitle(tr("Compose Mail")); } else { setWindowTitle(tr("%1 - Compose Mail").arg(ui->subject->text())); } } void ComposeWidget::toggleReplyMarking() { (m_actionInReplyTo->isChecked() ? m_actionStandalone : m_actionInReplyTo)->trigger(); } void ComposeWidget::updateReplyMarkingAction() { auto action = m_markAsReply->checkedAction(); m_actionToggleMarking->setText(action->text()); m_actionToggleMarking->setIcon(action->icon()); m_actionToggleMarking->setToolTip(action->toolTip()); } } diff --git a/src/Gui/SettingsDialog.cpp b/src/Gui/SettingsDialog.cpp index a6e9b6b6..2eeb8b12 100644 --- a/src/Gui/SettingsDialog.cpp +++ b/src/Gui/SettingsDialog.cpp @@ -1,1482 +1,1484 @@ /* Copyright (C) 2006 - 2016 Jan Kundrát Copyright (C) 2014 Luke Dashjr Copyright (C) 2012 Mohammed Nafees Copyright (C) 2013 Pali Rohár This file is part of the Trojita Qt IMAP e-mail client, http://trojita.flaska.net/ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License or (at your option) version 3 or any later version accepted by the membership of KDE e.V. (or its successor approved by the membership of KDE e.V.), which shall act as a proxy defined in Section 14 of version 3 of the license. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "SettingsDialog.h" #include "ColoredItemDelegate.h" #include "Common/InvokeMethod.h" #include "Common/PortNumbers.h" #include "Common/SettingsNames.h" #include "Gui/Util.h" #include "Gui/Window.h" #include "Imap/Model/ImapAccess.h" #include "MSA/Account.h" #include "Plugins/AddressbookPlugin.h" #include "Plugins/PasswordPlugin.h" #include "Plugins/PluginManager.h" #include "UiUtils/IconLoader.h" #include "UiUtils/PasswordWatcher.h" #include "ShortcutHandler/ShortcutHandler.h" namespace Gui { QString SettingsDialog::warningStyleSheet = Util::cssWarningBorder() + QStringLiteral("font-weight: bold;"); /** @short Check a text field for being non empty. If it's empty, show an error to the user. */ template bool checkProblemWithEmptyTextField(T *field, const QString &message) { if (field->text().isEmpty()) { QToolTip::showText(field->mapToGlobal(QPoint(10, field->height() / 2)), message, 0); return true; } else { return false; } } SettingsDialog::SettingsDialog(MainWindow *parent, Composer::SenderIdentitiesModel *identitiesModel, Imap::Mailbox::FavoriteTagsModel *favoriteTagsModel, QSettings *settings): QDialog(parent), mainWindow(parent), m_senderIdentities(identitiesModel), m_favoriteTags(favoriteTagsModel), m_settings(settings) { setWindowTitle(tr("Settings")); QVBoxLayout *layout = new QVBoxLayout(this); stack = new QTabWidget(this); layout->addWidget(stack); stack->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); addPage(new GeneralPage(this, *m_settings, m_senderIdentities), tr("&General")); addPage(new ImapPage(this, *m_settings), tr("I&MAP")); addPage(new CachePage(this, *m_settings), tr("&Offline")); addPage(new OutgoingPage(this, *m_settings), tr("&SMTP")); #ifdef XTUPLE_CONNECT addPage(xtConnect = new XtConnectPage(this, *m_settings, imap), tr("&xTuple")); #endif addPage(new FavoriteTagsPage(this, *m_settings, m_favoriteTags), tr("Favorite &tags")); buttons = new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel, Qt::Horizontal, this); connect(buttons, &QDialogButtonBox::accepted, this, &SettingsDialog::accept); connect(buttons, &QDialogButtonBox::rejected, this, &SettingsDialog::reject); layout->addWidget(buttons); EMIT_LATER_NOARG(this, reloadPasswordsRequested); } void SettingsDialog::setOriginalPlugins(const QString &passwordPlugin, const QString &addressBookPlugin, const QString &spellcheckerPlugin) { m_originalPasswordPlugin = passwordPlugin; m_originalAddressbookPlugin = addressBookPlugin; m_originalSpellcheckerPlugin = spellcheckerPlugin; } void SettingsDialog::adjustSizeToScrollAreas() { QScrollArea *area = qobject_cast(sender()); Q_ASSERT(area && area->widget()); // task #A: figure the "minimum" size for the tabwidget // #A.1: search scrollareas and align their size to their content // update size of the widget in the tabbed scrollarea area->widget()->adjustSize(); // figure the size demand of this scroll area (content + margins) int l,t,r,b; area->getContentsMargins(&l,&r,&t,&b); QSize minSize(area->widget()->size() + QSize(l+r, t+b)); // TODO: clamp this to 640x480 or QDesktopWidget::availableGeometry() dependent? // do not shrink (prevent nasty size jumps for no reason) minSize.setWidth(qMax(area->width(), minSize.width())); minSize.setHeight(qMax(area->height(), minSize.height())); // task #B: find the QStackedWidget inside the QTabWidget to determine its margins Q_FOREACH(const QObject *o, stack->children()) { if (const QStackedWidget *actualStack = qobject_cast(o)) { minSize.setWidth(minSize.width() + stack->width() - actualStack->width()); minSize.setHeight(minSize.height() + stack->height() - actualStack->height()); break; } } // task #C: convince the dialog to the new size // #C.1: arrest the tabwidget stack->setMinimumSize(minSize); // #C.2: force a relayout of the dialog (do NOT use "adjustSize", which may still shrink) layout()->activate(); // #C.3: release the tabwidget minimum size stack->setMinimumSize(QSize(0, 0)); } Plugins::PluginManager *SettingsDialog::pluginManager() { return mainWindow->pluginManager(); } Imap::ImapAccess *SettingsDialog::imapAccess() { return mainWindow->imapAccess(); } void SettingsDialog::accept() { m_saveSignalCount = 0; Q_FOREACH(ConfigurationWidgetInterface *page, pages) { if (!page->checkValidity()) { stack->setCurrentWidget(page->asWidget()); return; } connect(page->asWidget(), SIGNAL(saved()), this, SLOT(slotAccept())); // new-signal-slot: we're abusing the type system a bit here, cannot use the new syntax ++m_saveSignalCount; } #ifndef Q_OS_WIN // Try to wour around QSettings' inability to set umask for its file access. We don't want to set umask globally. QFile settingsFile(m_settings->fileName()); settingsFile.setPermissions(QFile::ReadUser | QFile::WriteUser); #endif buttons->setEnabled(false); Q_FOREACH(ConfigurationWidgetInterface *page, pages) { page->asWidget()->setEnabled(false); } Q_FOREACH(ConfigurationWidgetInterface *page, pages) { page->save(*m_settings); } #ifdef XTUPLE_CONNECT xtConnect->save(*m_settings); #endif m_settings->sync(); #ifndef Q_OS_WIN settingsFile.setPermissions(QFile::ReadUser | QFile::WriteUser); #endif } void SettingsDialog::slotAccept() { disconnect(sender(), SIGNAL(saved()), this, SLOT(slotAccept())); // new-signal-slot: we're abusing the type system a bit here, cannot use the new syntax if (--m_saveSignalCount > 0) { return; } QStringList passwordFailures; Q_FOREACH(ConfigurationWidgetInterface *page, pages) { QString message; if (page->passwordFailures(message)) { passwordFailures << message; } } if (!passwordFailures.isEmpty()) { Gui::Util::messageBoxWarning(this, tr("Saving passwords failed"), tr("

Couldn't save passwords. These were the error messages:

\n

%1

") .arg(passwordFailures.join(QStringLiteral("
")))); } buttons->setEnabled(true); QDialog::accept(); } void SettingsDialog::reject() { // The changes were performed on the live data, so we have to make sure they are discarded when user cancels #define HANDLE_PLUGIN(LOWERCASE, UPPERCASE) \ if (!m_original##UPPERCASE##Plugin.isEmpty() && pluginManager()->LOWERCASE##Plugin() != m_original##UPPERCASE##Plugin) { \ pluginManager()->set##UPPERCASE##Plugin(m_original##UPPERCASE##Plugin); \ } HANDLE_PLUGIN(addressbook, Addressbook) HANDLE_PLUGIN(password, Password) HANDLE_PLUGIN(spellchecker, Spellchecker) #undef HANDLE_PLUGIN m_senderIdentities->loadFromSettings(*m_settings); m_favoriteTags->loadFromSettings(*m_settings); QDialog::reject(); } void SettingsDialog::addPage(ConfigurationWidgetInterface *page, const QString &title) { stack->addTab(page->asWidget(), title); connect(page->asWidget(), SIGNAL(widgetsUpdated()), SLOT(adjustSizeToScrollAreas())); // new-signal-slot: we're abusing the type system a bit here, cannot use the new syntax QMetaObject::invokeMethod(page->asWidget(), "updateWidgets", Qt::QueuedConnection); pages << page; } FavoriteTagsPage::FavoriteTagsPage(SettingsDialog *parent, QSettings &s, Imap::Mailbox::FavoriteTagsModel *favoriteTagsModel): QScrollArea(parent), Ui_FavoriteTagsPage(), m_favoriteTagsModel(favoriteTagsModel), m_parent(parent) { Ui_FavoriteTagsPage::setupUi(this); Q_ASSERT(m_favoriteTagsModel); moveUpButton->setIcon(UiUtils::loadIcon(QStringLiteral("go-up"))); moveDownButton->setIcon(UiUtils::loadIcon(QStringLiteral("go-down"))); tagTableView->setModel(m_favoriteTagsModel); tagTableView->setItemDelegate(new ColoredItemDelegate(this)); tagTableView->setSelectionBehavior(QAbstractItemView::SelectRows); tagTableView->setSelectionMode(QAbstractItemView::SingleSelection); tagTableView->setGridStyle(Qt::NoPen); tagTableView->resizeRowsToContents(); tagTableView->horizontalHeader()->setStretchLastSection(true); // show tag name in color instead tagTableView->hideColumn(Imap::Mailbox::FavoriteTagsModel::COLUMN_COLOR); connect(tagTableView, &QAbstractItemView::clicked, this, &FavoriteTagsPage::updateWidgets); connect(tagTableView, &QAbstractItemView::doubleClicked, this, &FavoriteTagsPage::editButtonClicked); connect(m_favoriteTagsModel, &QAbstractItemModel::modelReset, this, &FavoriteTagsPage::updateWidgets); connect(m_favoriteTagsModel, &QAbstractItemModel::rowsInserted, this, &FavoriteTagsPage::updateWidgets); connect(m_favoriteTagsModel, &QAbstractItemModel::rowsRemoved, this, &FavoriteTagsPage::updateWidgets); connect(m_favoriteTagsModel, &QAbstractItemModel::dataChanged, this, &FavoriteTagsPage::updateWidgets); connect(moveUpButton, &QAbstractButton::clicked, this, [this](){ FavoriteTagsPage::moveTagBy(-1); }); connect(moveDownButton, &QAbstractButton::clicked, this, [this](){ FavoriteTagsPage::moveTagBy(1); }); connect(addButton, &QAbstractButton::clicked, this, &FavoriteTagsPage::addButtonClicked); connect(editButton, &QAbstractButton::clicked, this, &FavoriteTagsPage::editButtonClicked); connect(deleteButton, &QAbstractButton::clicked, this, &FavoriteTagsPage::deleteButtonClicked); updateWidgets(); } void FavoriteTagsPage::updateWidgets() { bool enabled = tagTableView->currentIndex().isValid(); deleteButton->setEnabled(enabled); editButton->setEnabled(enabled); bool upEnabled = m_favoriteTagsModel->rowCount() > 0 && tagTableView->currentIndex().row() > 0; bool downEnabled = m_favoriteTagsModel->rowCount() > 0 && tagTableView->currentIndex().isValid() && tagTableView->currentIndex().row() < m_favoriteTagsModel->rowCount() - 1; moveUpButton->setEnabled(upEnabled); moveDownButton->setEnabled(downEnabled); tagTableView->resizeColumnToContents(Imap::Mailbox::FavoriteTagsModel::COLUMN_INDEX); tagTableView->resizeColumnToContents(Imap::Mailbox::FavoriteTagsModel::COLUMN_NAME); emit widgetsUpdated(); } void FavoriteTagsPage::moveTagBy(const int offset) { int from = tagTableView->currentIndex().row(); int to = tagTableView->currentIndex().row() + offset; m_favoriteTagsModel->moveTag(from, to); updateWidgets(); } void FavoriteTagsPage::addButtonClicked() { m_favoriteTagsModel->appendTag(Imap::Mailbox::ItemFavoriteTagItem()); tagTableView->setCurrentIndex(m_favoriteTagsModel->index(m_favoriteTagsModel->rowCount() - 1, 0)); EditFavoriteTag *dialog = new EditFavoriteTag(this, m_favoriteTagsModel, tagTableView->currentIndex()); dialog->setDeleteOnReject(); dialog->setWindowTitle(tr("Add New Tag")); dialog->show(); updateWidgets(); } void FavoriteTagsPage::editButtonClicked() { EditFavoriteTag *dialog = new EditFavoriteTag(this, m_favoriteTagsModel, tagTableView->currentIndex()); dialog->setWindowTitle(tr("Edit Tag")); dialog->show(); } void FavoriteTagsPage::deleteButtonClicked() { Q_ASSERT(tagTableView->currentIndex().isValid()); m_favoriteTagsModel->removeTagAt(tagTableView->currentIndex().row()); updateWidgets(); } void FavoriteTagsPage::save(QSettings &s) { m_favoriteTagsModel->saveToSettings(s); emit saved(); } QWidget *FavoriteTagsPage::asWidget() { return this; } bool FavoriteTagsPage::checkValidity() const { return true; } bool FavoriteTagsPage::passwordFailures(QString &message) const { Q_UNUSED(message); return false; } GeneralPage::GeneralPage(SettingsDialog *parent, QSettings &s, Composer::SenderIdentitiesModel *identitiesModel): QScrollArea(parent), Ui_GeneralPage(), m_identitiesModel(identitiesModel), m_parent(parent) { Ui_GeneralPage::setupUi(this); Q_ASSERT(m_identitiesModel); editButton->setEnabled(false); deleteButton->setEnabled(false); moveUpButton->setIcon(UiUtils::loadIcon(QStringLiteral("go-up"))); moveDownButton->setIcon(UiUtils::loadIcon(QStringLiteral("go-down"))); moveUpButton->setEnabled(false); moveDownButton->setEnabled(false); identityTabelView->setModel(m_identitiesModel); identityTabelView->setSelectionBehavior(QAbstractItemView::SelectRows); identityTabelView->setSelectionMode(QAbstractItemView::SingleSelection); identityTabelView->setGridStyle(Qt::NoPen); identityTabelView->hideColumn(Composer::SenderIdentitiesModel::COLUMN_ORGANIZATION); identityTabelView->setColumnHidden(Composer::SenderIdentitiesModel::COLUMN_SIGNATURE, true); identityTabelView->resizeColumnToContents(Composer::SenderIdentitiesModel::COLUMN_NAME); identityTabelView->resizeRowsToContents(); identityTabelView->horizontalHeader()->setStretchLastSection(true); Plugins::PluginManager *pluginManager = parent->pluginManager(); QMap::const_iterator it; int i; #define HANDLE_PLUGIN(LOWERCASE, UPPERCASE, DISABLE, NOTFOUND) \ const QMap &LOWERCASE##Plugins = pluginManager->available##UPPERCASE##Plugins(); \ const QString &LOWERCASE##Plugin = pluginManager->LOWERCASE##Plugin(); \ int LOWERCASE##Index = -1; \ \ for (it = LOWERCASE##Plugins.constBegin(), i = 0; it != LOWERCASE##Plugins.constEnd(); ++it, ++i) { \ LOWERCASE##Box->addItem(it.value(), it.key()); \ if (LOWERCASE##Index < 0 && LOWERCASE##Plugin == it.key()) \ LOWERCASE##Index = i; \ } \ \ LOWERCASE##Box->addItem(DISABLE); \ \ if (LOWERCASE##Plugin == QLatin1String("none")) \ LOWERCASE##Index = LOWERCASE##Box->count()-1; \ \ if (LOWERCASE##Index == -1) { \ if (!LOWERCASE##Plugin.isEmpty()) \ LOWERCASE##Box->addItem(NOTFOUND.arg(LOWERCASE##Plugin), LOWERCASE##Plugin); \ LOWERCASE##Index = LOWERCASE##Box->count()-1; \ } \ \ LOWERCASE##Box->setCurrentIndex(LOWERCASE##Index); QString pluginNotFound = tr("Plugin not found (%1)"); HANDLE_PLUGIN(addressbook, Addressbook, tr("Disable address book"), pluginNotFound) HANDLE_PLUGIN(password, Password, tr("Disable passwords"), pluginNotFound) HANDLE_PLUGIN(spellchecker, Spellchecker, tr("Disable spell checking"), pluginNotFound) #undef HANDLE_PLUGIN m_parent->setOriginalPlugins(passwordPlugin, addressbookPlugin, spellcheckerPlugin); markReadCheckbox->setChecked(s.value(Common::SettingsNames::autoMarkReadEnabled, QVariant(true)).toBool()); markReadSeconds->setValue(s.value(Common::SettingsNames::autoMarkReadSeconds, QVariant(0)).toUInt()); connect(markReadCheckbox, &QAbstractButton::toggled, markReadSeconds, &QWidget::setEnabled); auto mboxDropAction = s.value(Common::SettingsNames::mboxDropAction, QVariant(QStringLiteral("ask"))).toString(); connect(mboxDropActionCheckbox, &QAbstractButton::toggled, mboxDropActionBox, &QWidget::setEnabled); if (mboxDropAction != QStringLiteral("ask")) mboxDropActionCheckbox->setChecked(true); mboxDropActionBox->addItem(tr("Move"), QStringLiteral("move")); if (mboxDropAction == QStringLiteral("move")) mboxDropActionBox->setCurrentIndex(mboxDropActionBox->count() - 1); mboxDropActionBox->addItem(tr("Copy"), QStringLiteral("copy")); if (mboxDropAction == QStringLiteral("copy")) mboxDropActionBox->setCurrentIndex(mboxDropActionBox->count() - 1); showHomepageCheckbox->setChecked(s.value(Common::SettingsNames::appLoadHomepage, QVariant(true)).toBool()); showHomepageCheckbox->setToolTip(trUtf8("

If enabled, Trojitá will show its homepage upon startup.

" "

The remote server will receive the user's IP address and versions of Trojitá, the Qt library, " "and the underlying operating system. No private information, like account settings " "or IMAP server details, are collected.

")); guiSystrayCheckbox->setChecked(s.value(Common::SettingsNames::guiShowSystray, QVariant(true)).toBool()); guiStartMinimizedCheckbox->setChecked(s.value(Common::SettingsNames::guiStartMinimized, QVariant(false)).toBool()); preferPlaintextCheckbox->setChecked(s.value(Common::SettingsNames::guiPreferPlaintextRendering).toBool()); revealTrojitaVersions->setChecked(s.value(Common::SettingsNames::interopRevealVersions, QVariant(true)).toBool()); connect(identityTabelView, &QAbstractItemView::clicked, this, &GeneralPage::updateWidgets); connect(identityTabelView, &QAbstractItemView::doubleClicked, this, &GeneralPage::editButtonClicked); connect(m_identitiesModel, &QAbstractItemModel::layoutChanged, this, &GeneralPage::updateWidgets); connect(m_identitiesModel, &QAbstractItemModel::rowsInserted, this, &GeneralPage::updateWidgets); connect(m_identitiesModel, &QAbstractItemModel::rowsRemoved, this, &GeneralPage::updateWidgets); connect(m_identitiesModel, &QAbstractItemModel::dataChanged, this, &GeneralPage::updateWidgets); connect(moveUpButton, &QAbstractButton::clicked, this, &GeneralPage::moveIdentityUp); connect(moveDownButton, &QAbstractButton::clicked, this, &GeneralPage::moveIdentityDown); connect(addButton, &QAbstractButton::clicked, this, &GeneralPage::addButtonClicked); connect(editButton, &QAbstractButton::clicked, this, &GeneralPage::editButtonClicked); connect(deleteButton, &QAbstractButton::clicked, this, &GeneralPage::deleteButtonClicked); connect(passwordBox, static_cast(&QComboBox::currentIndexChanged), this, &GeneralPage::passwordPluginChanged); connect(this, &GeneralPage::reloadPasswords, m_parent, &SettingsDialog::reloadPasswordsRequested); updateWidgets(); } void GeneralPage::passwordPluginChanged() { const QString &passwordPlugin = m_parent->pluginManager()->passwordPlugin(); const QString &selectedPasswordPlugin = passwordBox->itemData(passwordBox->currentIndex()).toString(); if (selectedPasswordPlugin != passwordPlugin) { m_parent->pluginManager()->setPasswordPlugin(selectedPasswordPlugin); emit reloadPasswords(); } } void GeneralPage::updateWidgets() { bool enabled = identityTabelView->currentIndex().isValid(); deleteButton->setEnabled(enabled); editButton->setEnabled(enabled); bool upEnabled = m_identitiesModel->rowCount() > 0 && identityTabelView->currentIndex().row() > 0; bool downEnabled = m_identitiesModel->rowCount() > 0 && identityTabelView->currentIndex().isValid() && identityTabelView->currentIndex().row() < m_identitiesModel->rowCount() - 1; moveUpButton->setEnabled(upEnabled); moveDownButton->setEnabled(downEnabled); identityTabelView->resizeColumnToContents(Composer::SenderIdentitiesModel::COLUMN_NAME); emit widgetsUpdated(); } void GeneralPage::moveIdentityUp() { int from = identityTabelView->currentIndex().row(); int to = identityTabelView->currentIndex().row() - 1; m_identitiesModel->moveIdentity(from, to); updateWidgets(); } void GeneralPage::moveIdentityDown() { int from = identityTabelView->currentIndex().row(); int to = identityTabelView->currentIndex().row() + 1; m_identitiesModel->moveIdentity(from, to); updateWidgets(); } void GeneralPage::addButtonClicked() { m_identitiesModel->appendIdentity(Composer::ItemSenderIdentity()); identityTabelView->setCurrentIndex(m_identitiesModel->index(m_identitiesModel->rowCount() - 1, 0)); EditIdentity *dialog = new EditIdentity(this, m_identitiesModel, identityTabelView->currentIndex()); dialog->setDeleteOnReject(); dialog->setWindowTitle(tr("Add New Identity")); dialog->show(); updateWidgets(); } void GeneralPage::editButtonClicked() { EditIdentity *dialog = new EditIdentity(this, m_identitiesModel, identityTabelView->currentIndex()); dialog->setWindowTitle(tr("Edit Identity")); dialog->show(); } void GeneralPage::deleteButtonClicked() { Q_ASSERT(identityTabelView->currentIndex().isValid()); QMessageBox::StandardButton answer = QMessageBox::question(this, tr("Delete Identity?"), tr("Are you sure you want to delete identity %1 <%2>?").arg( m_identitiesModel->index(identityTabelView->currentIndex().row(), Composer::SenderIdentitiesModel::COLUMN_NAME).data().toString(), m_identitiesModel->index(identityTabelView->currentIndex().row(), Composer::SenderIdentitiesModel::COLUMN_EMAIL).data().toString()), QMessageBox::Yes | QMessageBox::No); if (answer == QMessageBox::Yes) { m_identitiesModel->removeIdentityAt(identityTabelView->currentIndex().row()); updateWidgets(); } } void GeneralPage::save(QSettings &s) { m_identitiesModel->saveToSettings(s); s.setValue(Common::SettingsNames::autoMarkReadEnabled, markReadCheckbox->isChecked()); s.setValue(Common::SettingsNames::autoMarkReadSeconds, markReadSeconds->value()); s.setValue(Common::SettingsNames::mboxDropAction, mboxDropActionCheckbox->isChecked() ? mboxDropActionBox->currentData() : QStringLiteral("ask")); s.setValue(Common::SettingsNames::appLoadHomepage, showHomepageCheckbox->isChecked()); s.setValue(Common::SettingsNames::guiPreferPlaintextRendering, preferPlaintextCheckbox->isChecked()); s.setValue(Common::SettingsNames::guiShowSystray, guiSystrayCheckbox->isChecked()); s.setValue(Common::SettingsNames::guiStartMinimized, guiStartMinimizedCheckbox->isChecked()); s.setValue(Common::SettingsNames::interopRevealVersions, revealTrojitaVersions->isChecked()); #define HANDLE_PLUGIN(LOWERCASE, UPPERCASE) \ const QString &LOWERCASE##Plugin = m_parent->pluginManager()->LOWERCASE##Plugin(); \ const QString &selected##UPPERCASE##Plugin = LOWERCASE##Box->itemData(LOWERCASE##Box->currentIndex()).toString(); \ if (selected##UPPERCASE##Plugin != LOWERCASE##Plugin) { \ m_parent->pluginManager()->set##UPPERCASE##Plugin(selected##UPPERCASE##Plugin); \ } HANDLE_PLUGIN(addressbook, Addressbook); HANDLE_PLUGIN(password, Password); HANDLE_PLUGIN(spellchecker, Spellchecker); #undef HANDLE_PLUGIN emit saved(); } QWidget *GeneralPage::asWidget() { return this; } bool GeneralPage::checkValidity() const { if (m_identitiesModel->rowCount() < 1) { QToolTip::showText(identityTabelView->mapToGlobal(QPoint(10, identityTabelView->height() / 2)), tr("Please define some identities here"), 0); return false; } return true; } bool GeneralPage::passwordFailures(QString &message) const { Q_UNUSED(message); return false; } EditIdentity::EditIdentity(QWidget *parent, Composer::SenderIdentitiesModel *identitiesModel, const QModelIndex ¤tIndex): QDialog(parent), Ui_EditIdentity(), m_identitiesModel(identitiesModel), m_deleteOnReject(false) { Ui_EditIdentity::setupUi(this); m_mapper = new QDataWidgetMapper(this); m_mapper->setModel(m_identitiesModel); m_mapper->addMapping(realNameLineEdit, Composer::SenderIdentitiesModel::COLUMN_NAME); m_mapper->addMapping(emailLineEdit, Composer::SenderIdentitiesModel::COLUMN_EMAIL); m_mapper->addMapping(organisationLineEdit, Composer::SenderIdentitiesModel::COLUMN_ORGANIZATION); m_mapper->addMapping(signaturePlainTextEdit, Composer::SenderIdentitiesModel::COLUMN_SIGNATURE); m_mapper->setSubmitPolicy(QDataWidgetMapper::ManualSubmit); m_mapper->setCurrentIndex(currentIndex.row()); buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); connect(realNameLineEdit, &QLineEdit::textChanged, this, &EditIdentity::enableButton); connect(emailLineEdit, &QLineEdit::textChanged, this, &EditIdentity::enableButton); connect(organisationLineEdit, &QLineEdit::textChanged, this, &EditIdentity::enableButton); connect(signaturePlainTextEdit, &QPlainTextEdit::textChanged, this, &EditIdentity::enableButton); connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this, &QDialog::accept); connect(buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, this, &QDialog::reject); connect(this, &QDialog::accepted, m_mapper, &QDataWidgetMapper::submit); connect(this, &QDialog::rejected, this, &EditIdentity::onReject); setModal(true); signaturePlainTextEdit->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); } void EditIdentity::enableButton() { buttonBox->button(QDialogButtonBox::Ok)->setEnabled( !realNameLineEdit->text().isEmpty() && !emailLineEdit->text().isEmpty()); } /** @short If enabled, make sure that the current row gets deleted when the dialog is rejected */ void EditIdentity::setDeleteOnReject(const bool reject) { m_deleteOnReject = reject; } void EditIdentity::onReject() { if (m_deleteOnReject) m_identitiesModel->removeIdentityAt(m_mapper->currentIndex()); } EditFavoriteTag::EditFavoriteTag(QWidget *parent, Imap::Mailbox::FavoriteTagsModel *favoriteTagsModel, const QModelIndex ¤tIndex): QDialog(parent), Ui_EditFavoriteTag(), m_favoriteTagsModel(favoriteTagsModel), currentIndex(currentIndex), m_deleteOnReject(false) { Ui_EditFavoriteTag::setupUi(this); nameLineEdit->setText(name()); setColorButtonColor(color()); buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); connect(colorButton, &QAbstractButton::clicked, this, &EditFavoriteTag::colorButtonClick); connect(nameLineEdit, &QLineEdit::textChanged, this, &EditFavoriteTag::tryEnableButton); connect(buttonBox->button(QDialogButtonBox::Ok), &QAbstractButton::clicked, this, &QDialog::accept); connect(buttonBox->button(QDialogButtonBox::Cancel), &QAbstractButton::clicked, this, &QDialog::reject); connect(this, &QDialog::accepted, this, &EditFavoriteTag::onAccept); connect(this, &QDialog::rejected, this, &EditFavoriteTag::onReject); setModal(true); } QString EditFavoriteTag::name() { return m_favoriteTagsModel->data(m_favoriteTagsModel->index(currentIndex.row(), Imap::Mailbox::FavoriteTagsModel::COLUMN_NAME)).toString(); } QString EditFavoriteTag::color() { return m_favoriteTagsModel->data(m_favoriteTagsModel->index(currentIndex.row(), Imap::Mailbox::FavoriteTagsModel::COLUMN_COLOR)).toString(); } void EditFavoriteTag::setColorButtonColor(const QString color) { colorButton->setProperty("colorName", color); QPalette pal = colorButton->palette(); pal.setColor(QPalette::Button, QColor(color)); colorButton->setAutoFillBackground(true); colorButton->setPalette(pal); colorButton->setFlat(true); colorButton->update(); } void EditFavoriteTag::colorButtonClick() { const QColor color = QColorDialog::getColor(QColor(colorButton->property("colorName").toString()), this, tr("Select tag color")); if (color.isValid()) { setColorButtonColor(color.name()); tryEnableButton(); } } void EditFavoriteTag::tryEnableButton() { buttonBox->button(QDialogButtonBox::Ok)->setEnabled( !nameLineEdit->text().isEmpty() && QColor(colorButton->property("colorName").toString()).isValid() ); } /** @short If enabled, make sure that the current row gets deleted when the dialog is rejected */ void EditFavoriteTag::setDeleteOnReject(const bool reject) { m_deleteOnReject = reject; } void EditFavoriteTag::onAccept() { m_favoriteTagsModel->setData(m_favoriteTagsModel->index(currentIndex.row(), Imap::Mailbox::FavoriteTagsModel::COLUMN_NAME), nameLineEdit->text()); m_favoriteTagsModel->setData(m_favoriteTagsModel->index(currentIndex.row(), Imap::Mailbox::FavoriteTagsModel::COLUMN_COLOR), colorButton->property("colorName")); } void EditFavoriteTag::onReject() { if (m_deleteOnReject) m_favoriteTagsModel->removeTagAt(currentIndex.row()); } ImapPage::ImapPage(SettingsDialog *parent, QSettings &s): QScrollArea(parent), Ui_ImapPage(), m_parent(parent) { Ui_ImapPage::setupUi(this); method->insertItem(NETWORK, tr("Network Connection")); method->insertItem(PROCESS, tr("Local Process")); encryption->insertItem(NONE, tr("No encryption")); encryption->insertItem(STARTTLS, tr("Use encryption (STARTTLS)")); encryption->insertItem(SSL, tr("Force encryption (TLS)")); using Common::SettingsNames; int defaultImapPort = Common::PORT_IMAPS; if (s.value(SettingsNames::imapMethodKey).toString() == SettingsNames::methodTCP) { method->setCurrentIndex(NETWORK); if (s.value(SettingsNames::imapStartTlsKey,true).toBool()) encryption->setCurrentIndex(STARTTLS); else encryption->setCurrentIndex(NONE); defaultImapPort = Common::PORT_IMAP; } else if (s.value(SettingsNames::imapMethodKey).toString() == SettingsNames::methodSSL) { method->setCurrentIndex(NETWORK); encryption->setCurrentIndex(SSL); } else if (s.value(SettingsNames::imapMethodKey).toString() == SettingsNames::methodProcess) { method->setCurrentIndex(PROCESS); } else { // Default settings -- let's assume SSL and hope that users who just press Cancel will configure when they see // the network error... method->setCurrentIndex(NETWORK); encryption->setCurrentIndex(SSL); } imapHost->setText(s.value(SettingsNames::imapHostKey).toString()); imapPort->setText(s.value(SettingsNames::imapPortKey, QString::number(defaultImapPort)).toString()); imapPort->setValidator(new QIntValidator(1, 65535, this)); connect(imapPort, &QLineEdit::textChanged, this, &ImapPage::maybeShowPortWarning); connect(encryption, static_cast(&QComboBox::currentIndexChanged), this, &ImapPage::maybeShowPortWarning); connect(method, static_cast(&QComboBox::currentIndexChanged), this, &ImapPage::maybeShowPortWarning); connect(encryption, static_cast(&QComboBox::currentIndexChanged), this, &ImapPage::changePort); portWarning->setStyleSheet(SettingsDialog::warningStyleSheet); connect(imapPass, &QLineEdit::textChanged, this, &ImapPage::updateWidgets); imapUser->setText(s.value(SettingsNames::imapUserKey).toString()); processPath->setText(s.value(SettingsNames::imapProcessKey).toString()); imapCapabilitiesBlacklist->setText(s.value(SettingsNames::imapBlacklistedCapabilities).toStringList().join(QStringLiteral(" "))); imapUseSystemProxy->setChecked(s.value(SettingsNames::imapUseSystemProxy, true).toBool()); imapNeedsNetwork->setChecked(s.value(SettingsNames::imapNeedsNetwork, true).toBool()); imapIdleRenewal->setValue(s.value(SettingsNames::imapIdleRenewal, QVariant(29)).toInt()); imapNumberRefreshInterval->setValue(m_parent->imapAccess()->numberRefreshInterval()); accountIcon->setText(s.value(SettingsNames::imapAccountIcon).toString()); archiveFolderName->setText(s.value(SettingsNames::imapArchiveFolderName).toString().isEmpty() ? SettingsNames::imapDefaultArchiveFolderName : s.value(SettingsNames::imapArchiveFolderName).toString()); m_imapPort = s.value(SettingsNames::imapPortKey, QString::number(defaultImapPort)).value(); connect(method, static_cast(&QComboBox::currentIndexChanged), this, &ImapPage::updateWidgets); // FIXME: use another account-id m_pwWatcher = m_parent->imapAccess()->passwordWatcher(); connect(m_pwWatcher, &UiUtils::PasswordWatcher::stateChanged, this, &ImapPage::updateWidgets); connect(m_pwWatcher, &UiUtils::PasswordWatcher::savingFailed, this, &ImapPage::saved); connect(m_pwWatcher, &UiUtils::PasswordWatcher::savingDone, this, &ImapPage::saved); connect(m_pwWatcher, &UiUtils::PasswordWatcher::readingDone, this, &ImapPage::slotSetPassword); connect(m_parent, &SettingsDialog::reloadPasswordsRequested, imapPass, &QLineEdit::clear); connect(m_parent, &SettingsDialog::reloadPasswordsRequested, m_pwWatcher, &UiUtils::PasswordWatcher::reloadPassword); updateWidgets(); maybeShowPortWarning(); } void ImapPage::slotSetPassword() { imapPass->setText(m_pwWatcher->password()); } void ImapPage::changePort() { imapPort->setText(QString::number(encryption->currentIndex() == SSL ? Common::PORT_IMAPS : Common::PORT_IMAP)); } void ImapPage::updateWidgets() { QFormLayout *lay = formLayout; Q_ASSERT(lay); switch (method->currentIndex()) { case NETWORK: imapHost->setVisible(true); imapPort->setVisible(true); encryption->setVisible(true); lay->labelForField(imapHost)->setVisible(true); lay->labelForField(imapPort)->setVisible(true); lay->labelForField(encryption)->setVisible(true); processPath->setVisible(false); lay->labelForField(processPath)->setVisible(false); imapUseSystemProxy->setVisible(true); lay->labelForField(imapUseSystemProxy)->setVisible(true); // the "needs network" can very well apply to accounts using "local process" via SSH, so it is not disabled here break; default: imapHost->setVisible(false); imapPort->setVisible(false); encryption->setVisible(false); lay->labelForField(imapHost)->setVisible(false); lay->labelForField(imapPort)->setVisible(false); lay->labelForField(encryption)->setVisible(false); processPath->setVisible(true); lay->labelForField(processPath)->setVisible(true); imapUseSystemProxy->setVisible(false); lay->labelForField(imapUseSystemProxy)->setVisible(false); } switch (encryption->currentIndex()) { case NONE: case STARTTLS: if (imapPort->text().isEmpty() || imapPort->text() == QString::number(Common::PORT_IMAPS)) imapPort->setText(QString::number(Common::PORT_IMAP)); break; default: if (imapPort->text().isEmpty() || imapPort->text() == QString::number(Common::PORT_IMAP)) imapPort->setText(QString::number(Common::PORT_IMAPS)); } if (!m_pwWatcher->isPluginAvailable()) imapPass->setText(QString()); passwordWarning->setVisible(!imapPass->text().isEmpty()); if (m_pwWatcher->isStorageEncrypted()) { passwordWarning->setStyleSheet(QString()); passwordWarning->setText(trUtf8("This password will be saved in encrypted storage. " "If you do not enter password here, Trojitá will prompt for one when needed.")); } else { passwordWarning->setStyleSheet(SettingsDialog::warningStyleSheet); passwordWarning->setText(trUtf8("This password will be saved in clear text. " "If you do not enter password here, Trojitá will prompt for one when needed.")); } passwordPluginStatus->setVisible(!m_pwWatcher->isPluginAvailable() || m_pwWatcher->isWaitingForPlugin() || !m_pwWatcher->didReadOk() || !m_pwWatcher->didWriteOk()); passwordPluginStatus->setText(m_pwWatcher->progressMessage()); imapPass->setEnabled(m_pwWatcher->isPluginAvailable() && !m_pwWatcher->isWaitingForPlugin()); imapPassLabel->setEnabled(m_pwWatcher->isPluginAvailable() && !m_pwWatcher->isWaitingForPlugin()); emit widgetsUpdated(); } void ImapPage::save(QSettings &s) { using Common::SettingsNames; if (s.value(SettingsNames::imapHostKey) != imapHost->text()) { s.remove(Common::SettingsNames::imapSslPemPubKey); } switch (method->currentIndex()) { case NETWORK: if (imapHost->text().isEmpty()) { s.remove(SettingsNames::imapMethodKey); } else if (encryption->currentIndex() == NONE){ s.setValue(SettingsNames::imapMethodKey, SettingsNames::methodTCP); s.setValue(SettingsNames::imapStartTlsKey, false); } else if (encryption->currentIndex() == STARTTLS){ s.setValue(SettingsNames::imapMethodKey, SettingsNames::methodTCP); s.setValue(SettingsNames::imapStartTlsKey, true); } else { s.setValue(SettingsNames::imapMethodKey, SettingsNames::methodSSL); s.setValue(SettingsNames::imapStartTlsKey, true); } s.setValue(SettingsNames::imapHostKey, imapHost->text()); s.setValue(SettingsNames::imapPortKey, imapPort->text()); s.setValue(SettingsNames::imapUseSystemProxy, imapUseSystemProxy->isChecked()); break; default: if (processPath->text().isEmpty()) { s.remove(SettingsNames::imapMethodKey); } else { s.setValue(SettingsNames::imapMethodKey, SettingsNames::methodProcess); } s.setValue(SettingsNames::imapProcessKey, processPath->text()); } s.setValue(SettingsNames::imapUserKey, imapUser->text()); s.setValue(SettingsNames::imapBlacklistedCapabilities, imapCapabilitiesBlacklist->text().split(QStringLiteral(" "))); s.setValue(SettingsNames::imapNeedsNetwork, imapNeedsNetwork->isChecked()); s.setValue(SettingsNames::imapIdleRenewal, imapIdleRenewal->value()); m_parent->imapAccess()->setNumberRefreshInterval(imapNumberRefreshInterval->value()); s.setValue(SettingsNames::imapAccountIcon, accountIcon->text().isEmpty() ? QVariant() : QVariant(accountIcon->text())); s.setValue(SettingsNames::imapArchiveFolderName, archiveFolderName->text()); if (m_pwWatcher->isPluginAvailable() && !m_pwWatcher->isWaitingForPlugin()) { m_pwWatcher->setPassword(imapPass->text()); } else { emit saved(); } } QWidget *ImapPage::asWidget() { return this; } bool ImapPage::checkValidity() const { switch (method->currentIndex()) { case NETWORK: // We don't require the username, and that's on purpose. Some servers *could* possibly support PREAUTH :) if (checkProblemWithEmptyTextField(imapHost, tr("The IMAP server hostname is missing here"))) return false; break; default: // PREAUTH must definitely be supported here -- think imap-over-ssh-with-ssh-keys etc. if (checkProblemWithEmptyTextField(processPath, tr("The command line to the IMAP server is missing here. Perhaps you need to use SSL or TCP?"))) { return false; } break; } return true; } void ImapPage::maybeShowPortWarning() { if (method->currentIndex() == PROCESS) { portWarning->setVisible(false); return; } if (encryption->currentIndex() == SSL) { portWarning->setVisible(imapPort->text() != QString::number(Common::PORT_IMAPS)); portWarning->setText(tr("This port is nonstandard. The default port for IMAP secured over SSL/TLS is %1.").arg(Common::PORT_IMAPS)); } else { portWarning->setVisible(imapPort->text() != QString::number(Common::PORT_IMAP)); if (encryption->currentIndex() == STARTTLS) { portWarning->setText(tr("This port is nonstandard. The default port for IMAP secured via STARTTLS is %1.").arg(Common::PORT_IMAP)); } else { portWarning->setText(tr("This port is nonstandard. The default port for IMAP over cleartext is %1.").arg(Common::PORT_IMAP)); } } } bool ImapPage::passwordFailures(QString &message) const { if (!m_pwWatcher->isPluginAvailable() || m_pwWatcher->isWaitingForPlugin() || m_pwWatcher->didWriteOk()) { return false; } else { message = m_pwWatcher->progressMessage(); return true; } } CachePage::CachePage(QWidget *parent, QSettings &s): QScrollArea(parent), Ui_CachePage() { Ui_CachePage::setupUi(this); using Common::SettingsNames; QString val = s.value(SettingsNames::cacheOfflineKey).toString(); if (val == SettingsNames::cacheOfflineAll) { offlineEverything->setChecked(true); } else if (val == SettingsNames::cacheOfflineNone) { offlineNope->setChecked(true); } else { offlineXDays->setChecked(true); } offlineNumberOfDays->setValue(s.value(SettingsNames::cacheOfflineNumberDaysKey, QVariant(30)).toInt()); val = s.value(SettingsNames::watchedFoldersKey).toString(); if (val == Common::SettingsNames::watchAll) { watchAll->setChecked(true); } else if (val == Common::SettingsNames::watchSubscribed) { watchSubscribed->setChecked(true); } else { watchInbox->setChecked(true); } updateWidgets(); connect(offlineNope, &QAbstractButton::clicked, this, &CachePage::updateWidgets); connect(offlineXDays, &QAbstractButton::clicked, this, &CachePage::updateWidgets); connect(offlineEverything, &QAbstractButton::clicked, this, &CachePage::updateWidgets); } void CachePage::updateWidgets() { offlineNumberOfDays->setEnabled(offlineXDays->isChecked()); emit widgetsUpdated(); } void CachePage::save(QSettings &s) { using Common::SettingsNames; if (offlineEverything->isChecked()) s.setValue(SettingsNames::cacheOfflineKey, SettingsNames::cacheOfflineAll); else if (offlineXDays->isChecked()) s.setValue(SettingsNames::cacheOfflineKey, SettingsNames::cacheOfflineXDays); else s.setValue(SettingsNames::cacheOfflineKey, SettingsNames::cacheOfflineNone); s.setValue(SettingsNames::cacheOfflineNumberDaysKey, offlineNumberOfDays->value()); if (watchAll->isChecked()) { s.setValue(SettingsNames::watchedFoldersKey, SettingsNames::watchAll); } else if (watchSubscribed->isChecked()) { s.setValue(SettingsNames::watchedFoldersKey, SettingsNames::watchSubscribed); } else { s.setValue(SettingsNames::watchedFoldersKey, SettingsNames::watchOnlyInbox); } emit saved(); } QWidget *CachePage::asWidget() { return this; } bool CachePage::checkValidity() const { // Nothing really special for this class return true; } bool CachePage::passwordFailures(QString &message) const { Q_UNUSED(message); return false; } OutgoingPage::OutgoingPage(SettingsDialog *parent, QSettings &s): QScrollArea(parent), Ui_OutgoingPage(), m_parent(parent) { using Common::SettingsNames; Ui_OutgoingPage::setupUi(this); // FIXME: use another account-id at some point in future // we are now using the profile to avoid overwriting passwords of // other profiles in secure storage QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); m_smtpAccountSettings = new MSA::Account(this, &s, profileName); portWarningLabel->setStyleSheet(SettingsDialog::warningStyleSheet); method->insertItem(NETWORK, tr("Network")); method->insertItem(SENDMAIL, tr("Local sendmail-compatible")); method->insertItem(IMAP_SENDMAIL, tr("IMAP SENDMAIL Extension"));; encryption->insertItem(SMTP, tr("No encryption")); encryption->insertItem(SMTP_STARTTLS, tr("Use encryption (STARTTLS)")); encryption->insertItem(SSMTP, tr("Force encryption (TLS)")); encryption->setCurrentIndex(SSMTP); connect(method, static_cast(&QComboBox::currentIndexChanged), this, &OutgoingPage::slotSetSubmissionMethod); connect(encryption, static_cast(&QComboBox::currentIndexChanged), this, &OutgoingPage::slotSetSubmissionMethod); connect(m_smtpAccountSettings, &MSA::Account::submissionMethodChanged, this, &OutgoingPage::updateWidgets); connect(m_smtpAccountSettings, &MSA::Account::saveToImapChanged, this, &OutgoingPage::updateWidgets); connect(m_smtpAccountSettings, &MSA::Account::authenticateEnabledChanged, this, &OutgoingPage::updateWidgets); connect(m_smtpAccountSettings, &MSA::Account::reuseImapAuthenticationChanged, this, &OutgoingPage::updateWidgets); connect(smtpPass, &QLineEdit::textChanged, this, &OutgoingPage::updateWidgets); connect(smtpHost, &LineEdit::textEditingFinished, m_smtpAccountSettings, &MSA::Account::setServer); connect(smtpUser, &LineEdit::textEditingFinished, m_smtpAccountSettings, &MSA::Account::setUsername); connect(smtpPort, &LineEdit::textEditingFinished, this, &OutgoingPage::setPortByText); connect(m_smtpAccountSettings, &MSA::Account::showPortWarning, this, &OutgoingPage::showPortWarning); connect(smtpAuth, &QAbstractButton::toggled, m_smtpAccountSettings, &MSA::Account::setAuthenticateEnabled); connect(smtpAuthReuseImapCreds, &QAbstractButton::toggled, m_smtpAccountSettings, &MSA::Account::setReuseImapAuthentication); connect(saveToImap, &QAbstractButton::toggled, m_smtpAccountSettings, &MSA::Account::setSaveToImap); connect(saveFolderName, &LineEdit::textEditingFinished, m_smtpAccountSettings, &MSA::Account::setSentMailboxName); connect(smtpBurl, &QAbstractButton::toggled, m_smtpAccountSettings, &MSA::Account::setUseBurl); connect(sendmail, &LineEdit::textEditingFinished, m_smtpAccountSettings, &MSA::Account::setPathToSendmail); - m_pwWatcher = new UiUtils::PasswordWatcher(this, m_parent->pluginManager(), QStringLiteral("account-0"), QStringLiteral("smtp")); + m_pwWatcher = new UiUtils::PasswordWatcher(this, m_parent->pluginManager(), + profileName.isEmpty() ? QStringLiteral("account-0") : profileName, + QStringLiteral("smtp")); connect(m_pwWatcher, &UiUtils::PasswordWatcher::stateChanged, this, &OutgoingPage::updateWidgets); connect(m_pwWatcher, &UiUtils::PasswordWatcher::savingFailed, this, &OutgoingPage::saved); connect(m_pwWatcher, &UiUtils::PasswordWatcher::savingDone, this, &OutgoingPage::saved); connect(m_pwWatcher, &UiUtils::PasswordWatcher::readingDone, this, &OutgoingPage::slotSetPassword); connect(m_parent, &SettingsDialog::reloadPasswordsRequested, smtpPass, &QLineEdit::clear); connect(m_parent, &SettingsDialog::reloadPasswordsRequested, m_pwWatcher, &UiUtils::PasswordWatcher::reloadPassword); updateWidgets(); } void OutgoingPage::slotSetPassword() { smtpPass->setText(m_pwWatcher->password()); } void OutgoingPage::slotSetSubmissionMethod() { switch (method->currentIndex()) { case SENDMAIL: m_smtpAccountSettings->setSubmissionMethod(MSA::Account::Method::SENDMAIL); break; case IMAP_SENDMAIL: m_smtpAccountSettings->setSubmissionMethod(MSA::Account::Method::IMAP_SENDMAIL); break; case NETWORK: switch (encryption->currentIndex()) { case SMTP: m_smtpAccountSettings->setSubmissionMethod(MSA::Account::Method::SMTP); break; case SMTP_STARTTLS: m_smtpAccountSettings->setSubmissionMethod(MSA::Account::Method::SMTP_STARTTLS); break; case SSMTP: m_smtpAccountSettings->setSubmissionMethod(MSA::Account::Method::SSMTP); break; } break; default: Q_ASSERT(false); } // Toggle the default ports upon changing the delivery method smtpPort->setText(QString::number(m_smtpAccountSettings->port())); } void OutgoingPage::setPortByText(const QString &text) { m_smtpAccountSettings->setPort(text.toUShort()); } void OutgoingPage::updateWidgets() { QFormLayout *lay = formLayout; Q_ASSERT(lay); switch (m_smtpAccountSettings->submissionMethod()) { case MSA::Account::Method::SMTP: method->setCurrentIndex(NETWORK); encryption->setCurrentIndex(SMTP); break; case MSA::Account::Method::SMTP_STARTTLS: method->setCurrentIndex(NETWORK); encryption->setCurrentIndex(SMTP_STARTTLS); break; case MSA::Account::Method::SSMTP: method->setCurrentIndex(NETWORK); encryption->setCurrentIndex(SSMTP); break; case MSA::Account::Method::SENDMAIL: method->setCurrentIndex(SENDMAIL); encryption->setVisible(false); encryptionLabel->setVisible(false); break; case MSA::Account::Method::IMAP_SENDMAIL: method->setCurrentIndex(IMAP_SENDMAIL); encryption->setVisible(false); encryptionLabel->setVisible(false); break; } switch (m_smtpAccountSettings->submissionMethod()) { case MSA::Account::Method::SMTP: case MSA::Account::Method::SMTP_STARTTLS: case MSA::Account::Method::SSMTP: { encryption->setVisible(true); encryptionLabel->setVisible(true); smtpHost->setVisible(true); lay->labelForField(smtpHost)->setVisible(true); smtpHost->setText(m_smtpAccountSettings->server()); smtpPort->setVisible(true); lay->labelForField(smtpPort)->setVisible(true); smtpPort->setText(QString::number(m_smtpAccountSettings->port())); smtpPort->setValidator(new QIntValidator(1, 65535, this)); smtpAuth->setVisible(true); lay->labelForField(smtpAuth)->setVisible(true); bool authEnabled = m_smtpAccountSettings->authenticateEnabled(); smtpAuth->setChecked(authEnabled); smtpAuthReuseImapCreds->setVisible(authEnabled); lay->labelForField(smtpAuthReuseImapCreds)->setVisible(authEnabled); bool reuseImapCreds = m_smtpAccountSettings->reuseImapAuthentication(); smtpAuthReuseImapCreds->setChecked(reuseImapCreds); smtpUser->setVisible(authEnabled && !reuseImapCreds); lay->labelForField(smtpUser)->setVisible(authEnabled && !reuseImapCreds); smtpUser->setText(m_smtpAccountSettings->username()); sendmail->setVisible(false); lay->labelForField(sendmail)->setVisible(false); saveToImap->setVisible(true); lay->labelForField(saveToImap)->setVisible(true); saveToImap->setChecked(m_smtpAccountSettings->saveToImap()); smtpBurl->setVisible(saveToImap->isChecked()); lay->labelForField(smtpBurl)->setVisible(saveToImap->isChecked()); smtpBurl->setChecked(m_smtpAccountSettings->useBurl()); if (!m_pwWatcher->isPluginAvailable()) smtpPass->setText(QString()); passwordWarning->setVisible(authEnabled && !reuseImapCreds && !smtpPass->text().isEmpty()); if (m_pwWatcher->isStorageEncrypted()) { passwordWarning->setStyleSheet(QString()); passwordWarning->setText(trUtf8("This password will be saved in encrypted storage. " "If you do not enter password here, Trojitá will prompt for one when needed.")); } else { passwordWarning->setStyleSheet(SettingsDialog::warningStyleSheet); passwordWarning->setText(trUtf8("This password will be saved in clear text. " "If you do not enter password here, Trojitá will prompt for one when needed.")); } passwordPluginStatus->setVisible(authEnabled && !reuseImapCreds && (!m_pwWatcher->isPluginAvailable() || m_pwWatcher->isWaitingForPlugin() || !m_pwWatcher->didReadOk() || !m_pwWatcher->didWriteOk())); passwordPluginStatus->setText(m_pwWatcher->progressMessage()); smtpPass->setVisible(authEnabled && !reuseImapCreds); smtpPass->setEnabled(m_pwWatcher->isPluginAvailable() && !m_pwWatcher->isWaitingForPlugin()); lay->labelForField(smtpPass)->setVisible(authEnabled && !reuseImapCreds); lay->labelForField(smtpPass)->setEnabled(m_pwWatcher->isPluginAvailable() && !m_pwWatcher->isWaitingForPlugin()); break; } case MSA::Account::Method::SENDMAIL: case MSA::Account::Method::IMAP_SENDMAIL: encryption->setVisible(false); encryptionLabel->setVisible(false); smtpHost->setVisible(false); lay->labelForField(smtpHost)->setVisible(false); smtpPort->setVisible(false); lay->labelForField(smtpPort)->setVisible(false); showPortWarning(QString()); smtpAuth->setVisible(false); lay->labelForField(smtpAuth)->setVisible(false); smtpUser->setVisible(false); lay->labelForField(smtpUser)->setVisible(false); smtpPass->setVisible(false); lay->labelForField(smtpPass)->setVisible(false); passwordWarning->setVisible(false); passwordPluginStatus->setVisible(false); if (m_smtpAccountSettings->submissionMethod() == MSA::Account::Method::SENDMAIL) { sendmail->setVisible(true); lay->labelForField(sendmail)->setVisible(true); sendmail->setText(m_smtpAccountSettings->pathToSendmail()); if (sendmail->text().isEmpty()) sendmail->setText(Common::SettingsNames::sendmailDefaultCmd); saveToImap->setVisible(true); saveToImap->setChecked(m_smtpAccountSettings->saveToImap()); lay->labelForField(saveToImap)->setVisible(true); } else { sendmail->setVisible(false); lay->labelForField(sendmail)->setVisible(false); saveToImap->setChecked(true); saveToImap->setVisible(false); lay->labelForField(saveToImap)->setVisible(false); } smtpBurl->setVisible(false); lay->labelForField(smtpBurl)->setVisible(false); smtpBurl->setChecked(m_smtpAccountSettings->useBurl()); passwordPluginStatus->setVisible(false); } saveFolderName->setVisible(saveToImap->isChecked()); lay->labelForField(saveFolderName)->setVisible(saveToImap->isChecked()); saveFolderName->setText(m_smtpAccountSettings->sentMailboxName()); emit widgetsUpdated(); } void OutgoingPage::save(QSettings &s) { m_smtpAccountSettings->saveSettings(); if (smtpAuth->isVisibleTo(this) && smtpAuth->isChecked() && m_pwWatcher->isPluginAvailable() && !m_pwWatcher->isWaitingForPlugin()) { m_pwWatcher->setPassword(smtpPass->text()); } else { emit saved(); } } void OutgoingPage::showPortWarning(const QString &warning) { if (!warning.isEmpty()) { portWarningLabel->setVisible(true); portWarningLabel->setText(warning); } else { portWarningLabel->setVisible(false); } } QWidget *OutgoingPage::asWidget() { return this; } bool OutgoingPage::checkValidity() const { switch (m_smtpAccountSettings->submissionMethod()) { case MSA::Account::Method::SMTP: case MSA::Account::Method::SMTP_STARTTLS: case MSA::Account::Method::SSMTP: if (checkProblemWithEmptyTextField(smtpHost, tr("The SMTP server hostname is missing here"))) return false; if (smtpAuth->isChecked() && !smtpAuthReuseImapCreds->isChecked() && checkProblemWithEmptyTextField(smtpUser, tr("The SMTP username is missing here"))) return false; break; case MSA::Account::Method::SENDMAIL: if (checkProblemWithEmptyTextField(sendmail, tr("The SMTP server hostname is missing here"))) return false; break; case MSA::Account::Method::IMAP_SENDMAIL: break; } if (saveToImap->isChecked() && checkProblemWithEmptyTextField(saveFolderName, tr("Please specify the folder name here"))) return false; return true; } bool OutgoingPage::passwordFailures(QString &message) const { // The const_cast is needed as Qt4 does not define the arguement of isVisibleTo as const if (!smtpAuth->isVisibleTo(const_cast(this)) || !smtpAuth->isChecked() || !m_pwWatcher->isPluginAvailable() || m_pwWatcher->isWaitingForPlugin() || m_pwWatcher->didWriteOk()) { return false; } else { message = m_pwWatcher->progressMessage(); return true; } } #ifdef XTUPLE_CONNECT XtConnectPage::XtConnectPage(QWidget *parent, QSettings &s, ImapPage *imapPage): QWidget(parent), imap(imapPage) { // Take care not to clash with the cache of the GUI QString cacheLocation = Common::writablePath(Common::LOCATION_CACHE) + QString::fromAscii("xtconnect-trojita"); QFormLayout *layout = new QFormLayout(this); cacheDir = new QLineEdit(s.value(Common::SettingsNames::xtConnectCacheDirectory, cacheLocation).toString(), this); layout->addRow(tr("Cache Directory"), cacheDir); QGroupBox *box = new QGroupBox(tr("Mailboxes to synchronize"), this); QVBoxLayout *boxLayout = new QVBoxLayout(box); QListWidget *mailboxes = new QListWidget(box); mailboxes->addItems(s.value(Common::SettingsNames::xtSyncMailboxList).toStringList()); for (int i = 0; i < mailboxes->count(); ++i) { mailboxes->item(i)->setFlags(Qt::ItemIsEnabled); } mailboxes->setToolTip(tr("Please use context menu inside the main application to select mailboxes to synchronize")); boxLayout->addWidget(mailboxes); layout->addRow(box); QString optionHost = s.value(Common::SettingsNames::xtDbHost).toString(); int optionPort = s.value(Common::SettingsNames::xtDbPort, QVariant(5432)).toInt(); QString optionDbname = s.value(Common::SettingsNames::xtDbDbName).toString(); QString optionUsername = s.value(Common::SettingsNames::xtDbUser).toString(); QStringList args = QCoreApplication::arguments(); for (int i = 1; i < args.length(); i++) { if (args.at(i) == "-h" && args.length() > i) optionHost = args.at(++i); else if (args.at(i) == "-d" && args.length() > i) optionDbname = args.at(++i); else if (args.at(i) == "-p" && args.length() > i) optionPort = args.at(++i).toInt(); else if (args.at(i) == "-U" && args.length() > i) optionUsername = args.at(++i); } hostName = new QLineEdit(optionHost); layout->addRow(tr("DB Hostname"), hostName); port = new QSpinBox(); port->setRange(1, 65535); port->setValue(optionPort); layout->addRow(tr("DB Port"), port); dbName = new QLineEdit(optionDbname); layout->addRow(tr("DB Name"), dbName); username = new QLineEdit(optionUsername); layout->addRow(tr("DB Username"), username); imapPasswordWarning = new QLabel(tr("Please fill in all IMAP options, including the password, at the IMAP page. " "If you do not save the password, background synchronization will not run."), this); imapPasswordWarning->setWordWrap(true); imapPasswordWarning->setStyleSheet(SettingsDialog::warningStyleSheet); layout->addRow(imapPasswordWarning); debugLog = new QCheckBox(); layout->addRow(tr("Debugging"), debugLog); QPushButton *btn = new QPushButton(tr("Run xTuple Synchronization")); connect(btn, SIGNAL(clicked()), this, SLOT(runXtConnect())); layout->addRow(btn); } void XtConnectPage::save(QSettings &s) { s.setValue(Common::SettingsNames::xtConnectCacheDirectory, cacheDir->text()); s.setValue(Common::SettingsNames::xtDbHost, hostName->text()); s.setValue(Common::SettingsNames::xtDbPort, port->value()); s.setValue(Common::SettingsNames::xtDbDbName, dbName->text()); s.setValue(Common::SettingsNames::xtDbUser, username->text()); saveXtConfig(); emit saved(); } void XtConnectPage::saveXtConfig() { QSettings s(QSettings::UserScope, QString::fromAscii("xTuple.com"), QString::fromAscii("xTuple")); // Copy the IMAP settings Q_ASSERT(imap); imap->save(s); // XtConnect-specific stuff s.setValue(Common::SettingsNames::xtConnectCacheDirectory, cacheDir->text()); QStringList keys = QStringList() << Common::SettingsNames::xtSyncMailboxList << Common::SettingsNames::xtDbHost << Common::SettingsNames::xtDbPort << Common::SettingsNames::xtDbDbName << Common::SettingsNames::xtDbUser << Common::SettingsNames::imapSslPemPubKey; Q_FOREACH(const QString &key, keys) { s.setValue(key, QSettings().value(key)); } } void XtConnectPage::runXtConnect() { // First of all, let's save the XTuple-specific configuration to save useless debugging saveXtConfig(); QString path = QCoreApplication::applicationFilePath(); QStringList args; #ifdef Q_OS_WIN path = path.replace( QLatin1String("Gui/debug/trojita"), QLatin1String("XtConnect/debug/xtconnect-trojita")).replace( QLatin1String("Gui/release/trojita"), QLatin1String("XtConnect/release/xtconnect-trojita")); QString cmd = path; #else path = path.replace(QLatin1String("src/Gui/trojita"), QLatin1String("src/XtConnect/xtconnect-trojita")); args << QLatin1String("-e") << path; QString cmd = QLatin1String("xterm"); #endif if (! hostName->text().isEmpty()) args << QLatin1String("-h") << hostName->text(); if (port->value() != 5432) args << QLatin1String("-p") << QString::number(port->value()); if (! dbName->text().isEmpty()) args << QLatin1String("-d") << dbName->text(); if (! username->text().isEmpty()) args << QLatin1String("-U") << username->text(); QString password = QInputDialog::getText(this, tr("Database Connection"), tr("Password"), QLineEdit::Password); args << QLatin1String("-w") << password; if (debugLog->isChecked()) args << QLatin1String("--log") << cacheDir->text() + QLatin1String("/xt-trojita-log"); QProcess::startDetached(cmd, args); } void XtConnectPage::showEvent(QShowEvent *event) { if (imap) { imapPasswordWarning->setVisible(! imap->hasPassword()); } QWidget::showEvent(event); } bool ImapPage::hasPassword() const { return ! imapPass->text().isEmpty(); } #endif }