diff --git a/src/Gui/ComposeWidget.cpp b/src/Gui/ComposeWidget.cpp index 104d2790..158bd1bd 100644 --- a/src/Gui/ComposeWidget.cpp +++ b/src/Gui/ComposeWidget.cpp @@ -1,1860 +1,1860 @@ /* 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. " + err = tr("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..b2780c97 100644 --- a/src/Gui/SettingsDialog.cpp +++ b/src/Gui/SettingsDialog.cpp @@ -1,1482 +1,1482 @@ /* 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.

" + showHomepageCheckbox->setToolTip(tr("

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. " + passwordWarning->setText(tr("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. " + passwordWarning->setText(tr("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")); 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. " + passwordWarning->setText(tr("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. " + passwordWarning->setText(tr("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 } diff --git a/src/Gui/ShortcutHandler/ShortcutConfigDialog.cpp b/src/Gui/ShortcutHandler/ShortcutConfigDialog.cpp index 144c28e3..ee71a093 100644 --- a/src/Gui/ShortcutHandler/ShortcutConfigDialog.cpp +++ b/src/Gui/ShortcutHandler/ShortcutConfigDialog.cpp @@ -1,94 +1,94 @@ /* Copyright (C) 2012, 2013 by Glad Deschrijver 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 "ShortcutConfigDialog.h" #ifndef QT_NO_SHORTCUT #include #include "ShortcutConfigWidget.h" #include "ShortcutHandler.h" namespace Gui { ShortcutConfigDialog::ShortcutConfigDialog(QWidget *parent) : QDialog(parent) { setModal(true); - setWindowTitle(tr("Configure Shortcuts") + QLatin1String(" - ") + trUtf8("Trojitá")); + setWindowTitle(tr("Configure Shortcuts") + QLatin1String(" - ") + tr("Trojitá")); m_shortcutConfigWidget = new ShortcutConfigWidget(this); connect(m_shortcutConfigWidget, &ShortcutConfigWidget::shortcutsChanged, this, &ShortcutConfigDialog::shortcutsChanged); QDialogButtonBox *buttonBox = new QDialogButtonBox(this); buttonBox->addButton(QDialogButtonBox::Ok); buttonBox->addButton(QDialogButtonBox::Cancel); connect(buttonBox, &QDialogButtonBox::accepted, this, &ShortcutConfigDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &ShortcutConfigDialog::reject); QWidget *buttonWidget = new QWidget(this); QHBoxLayout *buttonLayout = new QHBoxLayout(buttonWidget); buttonLayout->addWidget(m_shortcutConfigWidget->clearButton()); buttonLayout->addWidget(m_shortcutConfigWidget->useDefaultButton()); buttonLayout->addStretch(); buttonLayout->addWidget(buttonBox); buttonLayout->setContentsMargins(0, 0, 0, 0); QVBoxLayout *mainLayout = new QVBoxLayout; mainLayout->addWidget(m_shortcutConfigWidget); mainLayout->addWidget(buttonWidget); setLayout(mainLayout); } ShortcutConfigDialog::~ShortcutConfigDialog() { } /***************************************************************************/ void ShortcutConfigDialog::setExclusivityGroups(const QList &groups) { m_shortcutConfigWidget->setExclusivityGroups(groups); } void ShortcutConfigDialog::setActionDescriptions(const QHash &actionDescriptions) { m_shortcutConfigWidget->setActionDescriptions(actionDescriptions); } /***************************************************************************/ void ShortcutConfigDialog::accept() { m_shortcutConfigWidget->accept(); QDialog::accept(); } void ShortcutConfigDialog::reject() { m_shortcutConfigWidget->reject(); QDialog::reject(); } } // namespace Gui #endif // QT_NO_SHORTCUT diff --git a/src/Gui/ShortcutHandler/ShortcutConfigWidget.cpp b/src/Gui/ShortcutHandler/ShortcutConfigWidget.cpp index 0299f428..ae84595a 100644 --- a/src/Gui/ShortcutHandler/ShortcutConfigWidget.cpp +++ b/src/Gui/ShortcutHandler/ShortcutConfigWidget.cpp @@ -1,312 +1,312 @@ /* Copyright (C) 2012, 2013 by Glad Deschrijver 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 "ShortcutConfigWidget.h" #ifndef QT_NO_SHORTCUT #include #include #include #include "ShortcutHandler.h" #include "Common/SettingsCategoryGuard.h" #include "UiUtils/IconLoader.h" namespace Gui { ShortcutConfigWidget::ShortcutConfigWidget(QWidget *parent) : QWidget(parent) , m_shortcutsShouldBeRestored(false) { ui.setupUi(this); - setWindowTitle(tr("Configure Shortcuts") + QLatin1String(" - ") + trUtf8("Trojitá")); + setWindowTitle(tr("Configure Shortcuts") + QLatin1String(" - ") + tr("Trojitá")); ui.shortcutTreeWidget->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); ui.shortcutTreeWidget->header()->setSectionResizeMode(1, QHeaderView::Stretch); ui.shortcutTreeWidget->setUniformRowHeights(true); // all rows have the same height ui.shortcutTreeWidget->installEventFilter(this); ui.gridLayout->setColumnStretch(2, 1); // make sure the buttons are not too large ui.gridLayout->setContentsMargins(0, 0, 0, 0); setFocusProxy(ui.shortcutTreeWidget); connect(ui.searchLineEdit, &QLineEdit::textChanged, this, &ShortcutConfigWidget::searchItems); connect(ui.clearPushButton, &QAbstractButton::clicked, this, &ShortcutConfigWidget::clearShortcut); connect(ui.useDefaultPushButton, &QAbstractButton::clicked, this, &ShortcutConfigWidget::restoreDefaultShortcut); } ShortcutConfigWidget::~ShortcutConfigWidget() { } QPushButton *ShortcutConfigWidget::clearButton() { return ui.clearPushButton; } QPushButton *ShortcutConfigWidget::useDefaultButton() { return ui.useDefaultPushButton; } /***************************************************************************/ void ShortcutConfigWidget::setExclusivityGroups(const QList &groups) { // this function must be called after all actions are added // we set a unique ID for each exclusivity group and we set the data of each toplevel item to the ID of the group to which it belongs // the ID must be a negative number, since eventFilter() assumes that the ID is either a negative number or the index of the corresponding action in m_actions m_exclusivityGroups = groups; const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount(); for (int i = 0; i < topLevelItemCount; ++i) { QTreeWidgetItem *topLevelItem = ui.shortcutTreeWidget->topLevelItem(i); const QString parentId = topLevelItem->text(0); for (int j = 0; j < m_exclusivityGroups.size(); ++j) if (m_exclusivityGroups.at(j).contains(parentId)) topLevelItem->setData(1, Qt::UserRole, -j-1); } } void ShortcutConfigWidget::addItem(const QString &actionName, const QString &text, const QString &shortcut, const QIcon &icon, const QString &parentId) { // search correct toplevel item int topLevelItemNumber = -1; const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount(); for (int i = 0; i < topLevelItemCount; ++i) { if (ui.shortcutTreeWidget->topLevelItem(i)->text(0) == parentId) { topLevelItemNumber = i; break; } } if (topLevelItemNumber < 0) { // toplevel item with name parentId doesn't exist yet, so create QTreeWidgetItem *item = new QTreeWidgetItem(ui.shortcutTreeWidget); item->setText(0, parentId); topLevelItemNumber = topLevelItemCount; item->setSizeHint(1, QSize(0, 1.5 * qApp->fontMetrics().height())); // since the second column is stretchable, it doesn't matter that the width of the size hint is set to 0 item->setData(1, Qt::UserRole, QString()); } // create item QString textWithoutAccelerator = text; QTreeWidgetItem *item = new QTreeWidgetItem(ui.shortcutTreeWidget->topLevelItem(topLevelItemNumber)); item->setText(0, textWithoutAccelerator.remove(QLatin1Char('&'))); item->setIcon(0, icon); item->setText(1, shortcut); item->setData(1, Qt::UserRole, actionName); // store objectName of the current action } void ShortcutConfigWidget::setActionDescriptions(const QHash &actionDescriptions) { m_actionDescriptions = actionDescriptions; ui.shortcutTreeWidget->scrollToItem(ui.shortcutTreeWidget->invisibleRootItem()->child(0)); ui.shortcutTreeWidget->clear(); for (QHash::const_iterator it = m_actionDescriptions.constBegin(); it != m_actionDescriptions.constEnd(); ++it) { ActionDescription actionDescription = it.value(); addItem(it.key(), actionDescription.text, actionDescription.shortcut, UiUtils::loadIcon(actionDescription.iconName), actionDescription.parentId); } ui.shortcutTreeWidget->expandAll(); } /***************************************************************************/ void ShortcutConfigWidget::searchItems(const QString &text) { const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount(); for (int i = 0; i < topLevelItemCount; ++i) { QTreeWidgetItem *topLevelItem = ui.shortcutTreeWidget->topLevelItem(i); const int childCount = topLevelItem->childCount(); for (int j = 0; j < childCount; ++j) { QTreeWidgetItem *childItem = topLevelItem->child(j); if (childItem->text(0).contains(text, Qt::CaseInsensitive) || childItem->text(1).contains(text, Qt::CaseInsensitive)) childItem->setHidden(false); else childItem->setHidden(true); } } } /***************************************************************************/ bool ShortcutConfigWidget::eventFilter(QObject *obj, QEvent *event) { Q_UNUSED(obj); if (event->type() != QEvent::KeyPress) return false; QKeyEvent *keyEvent = static_cast(event); QString keySequence; // don't allow modifiers to be handled as single keys and skip CapsLock and NumLock if (keyEvent->key() == Qt::Key_Control || keyEvent->key() == Qt::Key_Shift || keyEvent->key() == Qt::Key_Meta || keyEvent->key() == Qt::Key_Alt || keyEvent->key() == Qt::Key_Super_L || keyEvent->key() == Qt::Key_AltGr || keyEvent->key() == Qt::Key_CapsLock || keyEvent->key() == Qt::Key_NumLock) return false; // skip some particular keys (note that Qt::Key_Up and friends are used to navigate the list, in order to avoid that they interfere with the shortcut changing, we skip them) if (keyEvent->modifiers() == Qt::NoModifier && (keyEvent->key() == Qt::Key_Up || keyEvent->key() == Qt::Key_Down || keyEvent->key() == Qt::Key_Left || keyEvent->key() == Qt::Key_Right || keyEvent->key() == Qt::Key_PageUp || keyEvent->key() == Qt::Key_PageDown || keyEvent->key() == Qt::Key_Tab || keyEvent->key() == Qt::Key_Backtab || keyEvent->key() == Qt::Key_Return || keyEvent->key() == Qt::Key_Escape)) return false; if (keyEvent->key() == Qt::Key_Backtab) // the above doesn't catch "Shift+Tab" return false; // create string representation of the new shortcut if (keyEvent->modifiers() & Qt::ControlModifier) keySequence += tr("Ctrl+", "Shortcut modifier"); if (keyEvent->modifiers() & Qt::AltModifier) keySequence += tr("Alt+", "Shortcut modifier"); if (keyEvent->modifiers() & Qt::ShiftModifier) keySequence += tr("Shift+", "Shortcut modifier"); keySequence += QKeySequence(keyEvent->key()).toString(QKeySequence::PortableText); // replace shortcut in the list (but not yet for real, this is done when the user accepts the dialog) QTreeWidgetItem *currentItem = ui.shortcutTreeWidget->currentItem(); if (!currentItem) // this is the case when ui.shortcutTreeWidget is empty return false; const QString actionName = currentItem->data(1, Qt::UserRole).toString(); if (actionName.isEmpty()) // this is the case when a toplevel item is selected return false; // test whether the new shortcut is already defined for another action, if yes then ask the user to set an empty shortcut for the old action const QVariant parentId = currentItem->parent()->data(1, Qt::UserRole); const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount(); for (int i = 0; i < topLevelItemCount; ++i) { QTreeWidgetItem *topLevelItem = ui.shortcutTreeWidget->topLevelItem(i); if (topLevelItem->data(1, Qt::UserRole) != parentId) // only deal with actions in the same exclusivity group continue; const int childCount = topLevelItem->childCount(); for (int j = 0; j < childCount; ++j) { QTreeWidgetItem *childItem = topLevelItem->child(j); if (keySequence == childItem->text(1) && childItem->data(1, Qt::UserRole).toString() != actionName) { QMessageBox::StandardButton result = QMessageBox::warning(this, - tr("Shortcut Conflicts") + QLatin1String(" - ") + trUtf8("Trojitá"), + tr("Shortcut Conflicts") + QLatin1String(" - ") + tr("Trojitá"), tr("

The \"%1\" shortcut is ambiguous with the following shortcut:

" "

%2

Do you want to assign an empty shortcut to this action?

") .arg(keySequence, childItem->text(0)), QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Ok); if (result == QMessageBox::Ok) childItem->setText(1, QString()); else return false; } } } // finally we can set the new shortcut currentItem->setText(1, keySequence); return true; } void ShortcutConfigWidget::clearShortcut() { QTreeWidgetItem *currentItem = ui.shortcutTreeWidget->currentItem(); const QString actionName = currentItem->data(1, Qt::UserRole).toString(); if (actionName.isEmpty()) return; currentItem->setText(1, QString()); } void ShortcutConfigWidget::restoreDefaultShortcut() { QTreeWidgetItem *currentItem = ui.shortcutTreeWidget->currentItem(); const QString actionName = currentItem->data(1, Qt::UserRole).toString(); if (actionName.isEmpty()) return; currentItem->setText(1, m_actionDescriptions[actionName].defaultShortcut); } /***************************************************************************/ void ShortcutConfigWidget::accept() { // set shortcuts for real (but they are not yet saved in the settings on disk, this is done in writeSettings()) const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount(); for (int i = 0; i < topLevelItemCount; ++i) { QTreeWidgetItem *topLevelItem = ui.shortcutTreeWidget->topLevelItem(i); const int childCount = topLevelItem->childCount(); for (int j = 0; j < childCount; ++j) { QTreeWidgetItem *childItem = topLevelItem->child(j); const QString actionName = childItem->data(1, Qt::UserRole).toString(); m_actionDescriptions[actionName].shortcut = childItem->text(1); } } writeSettings(); Q_EMIT(shortcutsChanged(m_actionDescriptions)); } void ShortcutConfigWidget::reject() { if (!m_shortcutsShouldBeRestored) return; // restore unsaved shortcuts in the tree widget const int topLevelItemCount = ui.shortcutTreeWidget->topLevelItemCount(); for (int i = 0; i < topLevelItemCount; ++i) { QTreeWidgetItem *topLevelItem = ui.shortcutTreeWidget->topLevelItem(i); const int childCount = topLevelItem->childCount(); for (int j = 0; j < childCount; ++j) { QTreeWidgetItem *childItem = topLevelItem->child(j); const QString actionName = childItem->data(1, Qt::UserRole).toString(); childItem->setText(1, m_actionDescriptions[actionName].shortcut); } } m_shortcutsShouldBeRestored = false; } void ShortcutConfigWidget::showEvent(QShowEvent *event) { Q_UNUSED(event); reject(); // restore unsaved shortcuts in the tree widget before the configuration dialog is shown again m_shortcutsShouldBeRestored = true; ui.shortcutTreeWidget->sortByColumn(0, Qt::AscendingOrder); } /***************************************************************************/ /** * \see ShortcutHandler::readSettings() */ void ShortcutConfigWidget::writeSettings() { QSettings *settings = ShortcutHandler::instance()->settingsObject(); Q_ASSERT_X(settings, "ShortcutHandler", "no QSettings object found: a settings object should first be set using setSettingsObject() and then readSettings() should be called when initializing your program; note that this QSettings object should exist during the entire lifetime of the ShortcutHandler object and therefore not be deleted first"); Common::SettingsCategoryGuard guard(settings, QStringLiteral("ShortcutHandler")); settings->remove(QString()); settings->beginWriteArray(QStringLiteral("Shortcuts")); int index = 0; for (QHash::const_iterator it = m_actionDescriptions.constBegin(); it != m_actionDescriptions.constEnd(); ++it) { ActionDescription actionDescription = it.value(); const QString shortcut = actionDescription.shortcut; if (shortcut != actionDescription.defaultShortcut) { settings->setArrayIndex(index); settings->setValue(QStringLiteral("Action"), it.key()); settings->setValue(QStringLiteral("Shortcut"), actionDescription.shortcut); ++index; } } settings->endArray(); } } // namespace Gui #endif // QT_NO_SHORTCUT diff --git a/src/Gui/Window.cpp b/src/Gui/Window.cpp index f4c024d7..595fa4e4 100644 --- a/src/Gui/Window.cpp +++ b/src/Gui/Window.cpp @@ -1,2902 +1,2902 @@ /* Copyright (C) 2006 - 2015 Jan Kundrát Copyright (C) 2013 - 2015 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 #include #include #include "configure.cmake.h" #include "Common/Application.h" #include "Common/InvokeMethod.h" #include "Common/Paths.h" #include "Common/PortNumbers.h" #include "Common/SettingsNames.h" #include "Composer/Mailto.h" #include "Composer/SenderIdentitiesModel.h" #ifdef TROJITA_HAVE_CRYPTO_MESSAGES # ifdef TROJITA_HAVE_GPGMEPP # include "Cryptography/GpgMe++.h" # endif #endif #include "Imap/Model/ImapAccess.h" #include "Imap/Model/MailboxTree.h" #include "Imap/Model/Model.h" #include "Imap/Model/ModelWatcher.h" #include "Imap/Model/MsgListModel.h" #include "Imap/Model/NetworkWatcher.h" #include "Imap/Model/PrettyMailboxModel.h" #include "Imap/Model/PrettyMsgListModel.h" #include "Imap/Model/SpecialFlagNames.h" #include "Imap/Model/ThreadingMsgListModel.h" #include "Imap/Model/FavoriteTagsModel.h" #include "Imap/Model/Utils.h" #include "Imap/Tasks/ImapTask.h" #include "Imap/Network/FileDownloadManager.h" #include "MSA/ImapSubmit.h" #include "MSA/Sendmail.h" #include "MSA/SMTP.h" #include "Plugins/AddressbookPlugin.h" #include "Plugins/PasswordPlugin.h" #include "Plugins/PluginManager.h" #include "CompleteMessageWidget.h" #include "ComposeWidget.h" #include "MailBoxTreeView.h" #include "MessageListWidget.h" #include "MessageView.h" #include "MessageSourceWidget.h" #include "Gui/MessageHeadersWidget.h" #include "MsgListView.h" #include "OnePanelAtTimeWidget.h" #include "PasswordDialog.h" #include "ProtocolLoggerWidget.h" #include "SettingsDialog.h" #include "SimplePartWidget.h" #include "Streams/SocketFactory.h" #include "TaskProgressIndicator.h" #include "Util.h" #include "Window.h" #include "ShortcutHandler/ShortcutHandler.h" #include "ui_CreateMailboxDialog.h" #include "ui_AboutDialog.h" #include "Imap/Model/ModelTest/modeltest.h" #include "UiUtils/IconLoader.h" #include "UiUtils/QaimDfsIterator.h" /** @short All user-facing widgets and related classes */ namespace Gui { static const char * const netErrorUnseen = "net_error_unseen"; MainWindow::MainWindow(QSettings *settings): QMainWindow(), m_imapAccess(0), m_mainHSplitter(0), m_mainVSplitter(0), m_mainStack(0), m_layoutMode(LAYOUT_COMPACT), m_skipSavingOfUI(true), m_delayedStateSaving(0), m_actionSortNone(0), m_ignoreStoredPassword(false), m_settings(settings), m_pluginManager(0), m_networkErrorMessageBox(0), m_trayIcon(0) { setAttribute(Qt::WA_AlwaysShowToolTips); // m_pluginManager must be created before calling createWidgets m_pluginManager = new Plugins::PluginManager(this, m_settings, Common::SettingsNames::addressbookPlugin, Common::SettingsNames::passwordPlugin, Common::SettingsNames::spellcheckerPlugin); connect(m_pluginManager, &Plugins::PluginManager::pluginsChanged, this, &MainWindow::slotPluginsChanged); connect(m_pluginManager, &Plugins::PluginManager::pluginError, this, [this](const QString &errorMessage) { Gui::Util::messageBoxWarning(this, tr("Plugin Error"), //: The %1 placeholder is a full error message as provided by Qt, ready for human consumption. - trUtf8("A plugin failed to load, therefore some functionality might be lost. " + tr("A plugin failed to load, therefore some functionality might be lost. " "You might want to update your system or report a bug to your vendor." "\n\n%1").arg(errorMessage)); }); #ifdef TROJITA_HAVE_CRYPTO_MESSAGES Plugins::PluginManager::MimePartReplacers replacers; #ifdef TROJITA_HAVE_GPGMEPP replacers.emplace_back(std::make_shared()); #endif m_pluginManager->setMimePartReplacers(replacers); #endif // ImapAccess contains a wrapper for retrieving passwords through some plugin. // That PasswordWatcher is used by the SettingsDialog's widgets *and* by this class, // which means that ImapAccess has to be constructed before we go and open the settings dialog. // 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_imapAccess = new Imap::ImapAccess(this, m_settings, m_pluginManager, profileName); connect(m_imapAccess, &Imap::ImapAccess::cacheError, this, &MainWindow::cacheError); connect(m_imapAccess, &Imap::ImapAccess::checkSslPolicy, this, &MainWindow::checkSslPolicy, Qt::QueuedConnection); ShortcutHandler *shortcutHandler = new ShortcutHandler(this); shortcutHandler->setSettingsObject(m_settings); defineActions(); shortcutHandler->readSettings(); // must happen after defineActions() // must be created before calling createWidgets m_favoriteTags = new Imap::Mailbox::FavoriteTagsModel(this); m_favoriteTags->loadFromSettings(*m_settings); createWidgets(); Imap::migrateSettings(m_settings); m_senderIdentities = new Composer::SenderIdentitiesModel(this); m_senderIdentities->loadFromSettings(*m_settings); if (! m_settings->contains(Common::SettingsNames::imapMethodKey)) { QTimer::singleShot(0, this, SLOT(slotShowSettings())); } setupModels(); createActions(); createMenus(); slotToggleSysTray(); slotPluginsChanged(); slotFavoriteTagsChanged(); connect(m_favoriteTags, &QAbstractItemModel::modelReset, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::layoutChanged, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::rowsMoved, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::rowsInserted, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::rowsRemoved, this, &MainWindow::slotFavoriteTagsChanged); connect(m_favoriteTags, &QAbstractItemModel::dataChanged, this, &MainWindow::slotFavoriteTagsChanged); // Please note that Qt 4.6.1 really requires passing the method signature this way, *not* using the SLOT() macro QDesktopServices::setUrlHandler(QStringLiteral("mailto"), this, "slotComposeMailUrl"); QDesktopServices::setUrlHandler(QStringLiteral("x-trojita-manage-contact"), this, "slotManageContact"); slotUpdateWindowTitle(); CALL_LATER_NOARG(this, recoverDrafts); if (m_actionLayoutWide->isEnabled() && m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutWide) { m_actionLayoutWide->trigger(); } else if (m_settings->value(Common::SettingsNames::guiMainWindowLayout) == Common::SettingsNames::guiMainWindowLayoutOneAtTime) { m_actionLayoutOneAtTime->trigger(); } else { m_actionLayoutCompact->trigger(); } connect(qApp, &QGuiApplication::applicationStateChanged, this, [&](Qt::ApplicationState state) { if (state == Qt::ApplicationActive && m_networkErrorMessageBox && m_networkErrorMessageBox->property(netErrorUnseen).toBool()) { m_networkErrorMessageBox->setProperty(netErrorUnseen, false); m_networkErrorMessageBox->show(); } }); // Don't listen to QDesktopWidget::resized; that is emitted too early (when it gets fired, the screen size has changed, but // the workspace area is still the old one). Instead, listen to workAreaResized which gets emitted at an appropriate time. // The delay is still there to guarantee some smoothing; on jkt's box there are typically three events in a rapid sequence // (some of them most likely due to the fact that at first, the actual desktop gets resized, the plasma panel reacts // to that and only after the panel gets resized, the available size of "the rest" is correct again). // Which is why it makes sense to introduce some delay in there. The 0.5s delay is my best guess and "should work" (especially // because every change bumps the timer anyway, as Thomas pointed out). QTimer *delayedResize = new QTimer(this); delayedResize->setSingleShot(true); delayedResize->setInterval(500); connect(delayedResize, &QTimer::timeout, this, &MainWindow::desktopGeometryChanged); connect(qApp->desktop(), &QDesktopWidget::workAreaResized, delayedResize, static_cast(&QTimer::start)); m_skipSavingOfUI = false; } void MainWindow::defineActions() { ShortcutHandler *shortcutHandler = ShortcutHandler::instance(); shortcutHandler->defineAction(QStringLiteral("action_application_exit"), QStringLiteral("application-exit"), tr("E&xit"), QKeySequence::Quit); shortcutHandler->defineAction(QStringLiteral("action_compose_mail"), QStringLiteral("document-edit"), tr("&New Message..."), QKeySequence::New); shortcutHandler->defineAction(QStringLiteral("action_compose_draft"), QStringLiteral("document-open-recent"), tr("&Edit Draft...")); shortcutHandler->defineAction(QStringLiteral("action_show_menubar"), QStringLiteral("view-list-text"), tr("Show Main Menu &Bar"), tr("Ctrl+M")); shortcutHandler->defineAction(QStringLiteral("action_expunge"), QStringLiteral("trash-empty"), tr("Exp&unge"), tr("Ctrl+E")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_read"), QStringLiteral("mail-mark-read"), tr("Mark as &Read"), QStringLiteral("M")); shortcutHandler->defineAction(QStringLiteral("action_go_to_next_unread"), QStringLiteral("arrow-right"), tr("&Next Unread Message"), QStringLiteral("N")); shortcutHandler->defineAction(QStringLiteral("action_go_to_previous_unread"), QStringLiteral("arrow-left"), tr("&Previous Unread Message"), QStringLiteral("P")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_deleted"), QStringLiteral("list-remove"), tr("Mark as &Deleted"), QKeySequence(Qt::Key_Delete).toString()); shortcutHandler->defineAction(QStringLiteral("action_mark_as_flagged"), QStringLiteral("mail-flagged"), tr("Mark as &Flagged"), QStringLiteral("S")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_junk"), QStringLiteral("mail-mark-junk"), tr("Mark as &Junk"), QStringLiteral("J")); shortcutHandler->defineAction(QStringLiteral("action_mark_as_notjunk"), QStringLiteral("mail-mark-notjunk"), tr("Mark as Not &junk"), QStringLiteral("Shift+J")); shortcutHandler->defineAction(QStringLiteral("action_save_message_as"), QStringLiteral("document-save"), tr("&Save Message...")); shortcutHandler->defineAction(QStringLiteral("action_view_message_source"), QString(), tr("View Message &Source...")); shortcutHandler->defineAction(QStringLiteral("action_view_message_headers"), QString(), tr("View Message &Headers..."), tr("Ctrl+U")); shortcutHandler->defineAction(QStringLiteral("action_reply_private"), QStringLiteral("mail-reply-sender"), tr("&Private Reply"), tr("Ctrl+Shift+A")); shortcutHandler->defineAction(QStringLiteral("action_reply_all_but_me"), QStringLiteral("mail-reply-all"), tr("Reply to All &but Me"), tr("Ctrl+Shift+R")); shortcutHandler->defineAction(QStringLiteral("action_reply_all"), QStringLiteral("mail-reply-all"), tr("Reply to &All"), tr("Ctrl+Alt+Shift+R")); shortcutHandler->defineAction(QStringLiteral("action_reply_list"), QStringLiteral("mail-reply-list"), tr("Reply to &Mailing List"), tr("Ctrl+L")); shortcutHandler->defineAction(QStringLiteral("action_reply_guess"), QString(), tr("Reply by &Guess"), tr("Ctrl+R")); shortcutHandler->defineAction(QStringLiteral("action_forward_attachment"), QStringLiteral("mail-forward"), tr("&Forward"), tr("Ctrl+Shift+F")); shortcutHandler->defineAction(QStringLiteral("action_resend"), QStringLiteral("mail-resend"), tr("Resend...")); shortcutHandler->defineAction(QStringLiteral("action_archive"), QStringLiteral("mail-move-to-archive"), tr("&Archive"), QStringLiteral("A")); shortcutHandler->defineAction(QStringLiteral("action_contact_editor"), QStringLiteral("contact-unknown"), tr("Address Book...")); shortcutHandler->defineAction(QStringLiteral("action_network_offline"), QStringLiteral("network-disconnect"), tr("&Offline")); shortcutHandler->defineAction(QStringLiteral("action_network_expensive"), QStringLiteral("network-wireless"), tr("&Expensive Connection")); shortcutHandler->defineAction(QStringLiteral("action_network_online"), QStringLiteral("network-connect"), tr("&Free Access")); shortcutHandler->defineAction(QStringLiteral("action_messagewindow_close"), QStringLiteral("window-close"), tr("Close Standalone Message Window")); shortcutHandler->defineAction(QStringLiteral("action_open_messagewindow"), QString(), tr("Open message in New Window..."), QStringLiteral("Ctrl+Return")); shortcutHandler->defineAction(QStringLiteral("action_oneattime_go_back"), QStringLiteral("go-previous"), tr("Navigate Back"), QKeySequence(QKeySequence::Back).toString()); shortcutHandler->defineAction(QStringLiteral("action_zoom_in"), QStringLiteral("zoom-in"), tr("Zoom In"), QKeySequence::ZoomIn); shortcutHandler->defineAction(QStringLiteral("action_zoom_out"), QStringLiteral("zoom-out"), tr("Zoom Out"), QKeySequence::ZoomOut); shortcutHandler->defineAction(QStringLiteral("action_zoom_original"), QStringLiteral("zoom-original"), tr("Original Size")); shortcutHandler->defineAction(QStringLiteral("action_focus_mailbox_tree"), QString(), tr("Move Focus to Mailbox List")); shortcutHandler->defineAction(QStringLiteral("action_focus_msg_list"), QString(), tr("Move Focus to Message List")); shortcutHandler->defineAction(QStringLiteral("action_focus_quick_search"), QString(), tr("Move Focus to Quick Search"), QStringLiteral("/")); shortcutHandler->defineAction(QStringLiteral("action_tag_1"), QStringLiteral("mail-tag-1"), tr("Tag with &1st tag"), QStringLiteral("1")); shortcutHandler->defineAction(QStringLiteral("action_tag_2"), QStringLiteral("mail-tag-2"), tr("Tag with &2nd tag"), QStringLiteral("2")); shortcutHandler->defineAction(QStringLiteral("action_tag_3"), QStringLiteral("mail-tag-3"), tr("Tag with &3rd tag"), QStringLiteral("3")); shortcutHandler->defineAction(QStringLiteral("action_tag_4"), QStringLiteral("mail-tag-4"), tr("Tag with &4th tag"), QStringLiteral("4")); shortcutHandler->defineAction(QStringLiteral("action_tag_5"), QStringLiteral("mail-tag-5"), tr("Tag with &5th tag"), QStringLiteral("5")); shortcutHandler->defineAction(QStringLiteral("action_tag_6"), QStringLiteral("mail-tag-6"), tr("Tag with &6th tag"), QStringLiteral("6")); shortcutHandler->defineAction(QStringLiteral("action_tag_7"), QStringLiteral("mail-tag-7"), tr("Tag with &7th tag"), QStringLiteral("7")); shortcutHandler->defineAction(QStringLiteral("action_tag_8"), QStringLiteral("mail-tag-8"), tr("Tag with &8th tag"), QStringLiteral("8")); shortcutHandler->defineAction(QStringLiteral("action_tag_9"), QStringLiteral("mail-tag-9"), tr("Tag with &9th tag"), QStringLiteral("9")); } void MainWindow::createActions() { // The shortcuts are a little bit complicated, unfortunately. This is what the other applications use by default: // // Thunderbird: // private: Ctrl+R // all: Ctrl+Shift+R // list: Ctrl+Shift+L // forward: Ctrl+L // (no shortcuts for type of forwarding) // resend: ctrl+B // new message: Ctrl+N // // KMail: // "reply": R // private: Shift+A // all: A // list: L // forward as attachment: F // forward inline: Shift+F // resend: E // new: Ctrl+N m_actionContactEditor = ShortcutHandler::instance()->createAction(QStringLiteral("action_contact_editor"), this, SLOT(invokeContactEditor()), this); m_mainToolbar = addToolBar(tr("Navigation")); m_mainToolbar->setObjectName(QStringLiteral("mainToolbar")); reloadMboxList = new QAction(style()->standardIcon(QStyle::SP_ArrowRight), tr("&Update List of Child Mailboxes"), this); connect(reloadMboxList, &QAction::triggered, this, &MainWindow::slotReloadMboxList); resyncMbox = new QAction(UiUtils::loadIcon(QStringLiteral("view-refresh")), tr("Check for &New Messages"), this); connect(resyncMbox, &QAction::triggered, this, &MainWindow::slotResyncMbox); reloadAllMailboxes = new QAction(tr("&Reload Everything"), this); // connect later exitAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_application_exit"), qApp, SLOT(quit()), this); exitAction->setStatusTip(tr("Exit the application")); netOffline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_offline")); netOffline->setCheckable(true); // connect later netExpensive = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_expensive")); netExpensive->setCheckable(true); // connect later netOnline = ShortcutHandler::instance()->createAction(QStringLiteral("action_network_online")); netOnline->setCheckable(true); // connect later QActionGroup *netPolicyGroup = new QActionGroup(this); netPolicyGroup->setExclusive(true); netPolicyGroup->addAction(netOffline); netPolicyGroup->addAction(netExpensive); netPolicyGroup->addAction(netOnline); //: a debugging tool showing the full contents of the whole IMAP server; all folders, messages and their parts showFullView = new QAction(UiUtils::loadIcon(QStringLiteral("edit-find-mail")), tr("Show Full &Tree Window"), this); showFullView->setCheckable(true); connect(showFullView, &QAction::triggered, allDock, &QWidget::setVisible); connect(allDock, &QDockWidget::visibilityChanged, showFullView, &QAction::setChecked); //: list of active "tasks", entities which are performing certain action like downloading a message or syncing a mailbox showTaskView = new QAction(tr("Show ImapTask t&ree"), this); showTaskView->setCheckable(true); connect(showTaskView, &QAction::triggered, taskDock, &QWidget::setVisible); connect(taskDock, &QDockWidget::visibilityChanged, showTaskView, &QAction::setChecked); //: a debugging tool showing the mime tree of the current message showMimeView = new QAction(tr("Show &MIME tree"), this); showMimeView->setCheckable(true); connect(showMimeView, &QAction::triggered, mailMimeDock, &QWidget::setVisible); connect(mailMimeDock, &QDockWidget::visibilityChanged, showMimeView, &QAction::setChecked); showProtocolLogger = new QAction(tr("Show protocol &log"), this); showProtocolLogger->setCheckable(true); connect(showProtocolLogger, &QAction::toggled, protocolLoggerDock, &QWidget::setVisible); connect(protocolLoggerDock, &QDockWidget::visibilityChanged, showProtocolLogger, &QAction::setChecked); //: file to save the debug log into logPersistent = new QAction(tr("Log &into %1").arg(Imap::Mailbox::persistentLogFileName()), this); logPersistent->setCheckable(true); connect(logPersistent, &QAction::triggered, protocolLogger, &ProtocolLoggerWidget::slotSetPersistentLogging); connect(protocolLogger, &ProtocolLoggerWidget::persistentLoggingChanged, logPersistent, &QAction::setChecked); showImapCapabilities = new QAction(tr("IMAP Server In&formation..."), this); connect(showImapCapabilities, &QAction::triggered, this, &MainWindow::slotShowImapInfo); showMenuBar = ShortcutHandler::instance()->createAction(QStringLiteral("action_show_menubar"), this); showMenuBar->setCheckable(true); showMenuBar->setChecked(true); connect(showMenuBar, &QAction::triggered, menuBar(), &QMenuBar::setVisible); connect(showMenuBar, &QAction::triggered, m_delayedStateSaving, static_cast(&QTimer::start)); showToolBar = new QAction(tr("Show &Toolbar"), this); showToolBar->setCheckable(true); connect(showToolBar, &QAction::triggered, m_mainToolbar, &QWidget::setVisible); connect(m_mainToolbar, &QToolBar::visibilityChanged, showToolBar, &QAction::setChecked); connect(m_mainToolbar, &QToolBar::visibilityChanged, m_delayedStateSaving, static_cast(&QTimer::start)); configSettings = new QAction(UiUtils::loadIcon(QStringLiteral("configure")), tr("&Settings..."), this); connect(configSettings, &QAction::triggered, this, &MainWindow::slotShowSettings); QAction *triggerSearch = new QAction(this); addAction(triggerSearch); triggerSearch->setShortcut(QKeySequence(QStringLiteral(":, ="))); connect(triggerSearch, &QAction::triggered, msgListWidget, &MessageListWidget::focusRawSearch); addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_quick_search"), msgListWidget, SLOT(focusSearch()), this)); addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_mailbox_tree"), mboxTree, SLOT(setFocus()), this)); addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_focus_msg_list"), msgListWidget->tree, SLOT(setFocus()), this)); m_oneAtTimeGoBack = ShortcutHandler::instance()->createAction(QStringLiteral("action_oneattime_go_back"), this); m_oneAtTimeGoBack->setEnabled(false); composeMail = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_mail"), this, SLOT(slotComposeMail()), this); m_editDraft = ShortcutHandler::instance()->createAction(QStringLiteral("action_compose_draft"), this, SLOT(slotEditDraft()), this); expunge = ShortcutHandler::instance()->createAction(QStringLiteral("action_expunge"), this, SLOT(slotExpunge()), this); m_forwardAsAttachment = ShortcutHandler::instance()->createAction(QStringLiteral("action_forward_attachment"), this, SLOT(slotForwardAsAttachment()), this); m_resend = ShortcutHandler::instance()->createAction(QStringLiteral("action_resend"), this, SLOT(slotResend()), this); markAsRead = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_read"), this); markAsRead->setCheckable(true); msgListWidget->tree->addAction(markAsRead); connect(markAsRead, &QAction::triggered, this, &MainWindow::handleMarkAsRead); m_nextMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_next_unread"), this, SLOT(slotNextUnread()), this); msgListWidget->tree->addAction(m_nextMessage); m_messageWidget->messageView->addAction(m_nextMessage); m_previousMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_go_to_previous_unread"), this, SLOT(slotPreviousUnread()), this); msgListWidget->tree->addAction(m_previousMessage); m_messageWidget->messageView->addAction(m_previousMessage); markAsDeleted = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_deleted"), this); markAsDeleted->setCheckable(true); msgListWidget->tree->addAction(markAsDeleted); connect(markAsDeleted, &QAction::triggered, this, &MainWindow::handleMarkAsDeleted); markAsFlagged = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_flagged"), this); markAsFlagged->setCheckable(true); msgListWidget->tree->addAction(markAsFlagged); connect(markAsFlagged, &QAction::triggered, this, &MainWindow::handleMarkAsFlagged); markAsJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_junk"), this); markAsJunk->setCheckable(true); msgListWidget->tree->addAction(markAsJunk); connect(markAsJunk, &QAction::triggered, this, &MainWindow::handleMarkAsJunk); markAsNotJunk = ShortcutHandler::instance()->createAction(QStringLiteral("action_mark_as_notjunk"), this); markAsNotJunk->setCheckable(true); msgListWidget->tree->addAction(markAsNotJunk); connect(markAsNotJunk, &QAction::triggered, this, &MainWindow::handleMarkAsNotJunk); saveWholeMessage = ShortcutHandler::instance()->createAction(QStringLiteral("action_save_message_as"), this, SLOT(slotSaveCurrentMessageBody()), this); msgListWidget->tree->addAction(saveWholeMessage); viewMsgSource = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_source"), this, SLOT(slotViewMsgSource()), this); msgListWidget->tree->addAction(viewMsgSource); viewMsgHeaders = ShortcutHandler::instance()->createAction(QStringLiteral("action_view_message_headers"), this, SLOT(slotViewMsgHeaders()), this); msgListWidget->tree->addAction(viewMsgHeaders); msgListWidget->tree->addAction(ShortcutHandler::instance()->createAction(QStringLiteral("action_open_messagewindow"), this, SLOT(openCompleteMessageWidget()), this)); moveToArchive = ShortcutHandler::instance()->createAction(QStringLiteral("action_archive"), this); msgListWidget->tree->addAction(moveToArchive); connect(moveToArchive, &QAction::triggered, this, &MainWindow::handleMoveToArchive); auto addTagAction = [=](int row) { QAction *tag = ShortcutHandler::instance()->createAction(QStringLiteral("action_tag_").append(QString::number(row)), this); tag->setCheckable(true); msgListWidget->tree->addAction(tag); connect(tag, &QAction::triggered, this, [=](const bool checked) { handleTag(checked, row - 1); }); return tag; }; tag1 = addTagAction(1); tag2 = addTagAction(2); tag3 = addTagAction(3); tag4 = addTagAction(4); tag5 = addTagAction(5); tag6 = addTagAction(6); tag7 = addTagAction(7); tag8 = addTagAction(8); tag9 = addTagAction(9); //: "mailbox" as a "folder of messages", not as a "mail account" createChildMailbox = new QAction(tr("Create &Child Mailbox..."), this); connect(createChildMailbox, &QAction::triggered, this, &MainWindow::slotCreateMailboxBelowCurrent); //: "mailbox" as a "folder of messages", not as a "mail account" createTopMailbox = new QAction(tr("Create &New Mailbox..."), this); connect(createTopMailbox, &QAction::triggered, this, &MainWindow::slotCreateTopMailbox); m_actionMarkMailboxAsRead = new QAction(tr("&Mark Mailbox as Read"), this); connect(m_actionMarkMailboxAsRead, &QAction::triggered, this, &MainWindow::slotMarkCurrentMailboxRead); //: "mailbox" as a "folder of messages", not as a "mail account" deleteCurrentMailbox = new QAction(tr("&Remove Mailbox"), this); connect(deleteCurrentMailbox, &QAction::triggered, this, &MainWindow::slotDeleteCurrentMailbox); #ifdef XTUPLE_CONNECT xtIncludeMailboxInSync = new QAction(tr("&Synchronize with xTuple"), this); xtIncludeMailboxInSync->setCheckable(true); connect(xtIncludeMailboxInSync, SIGNAL(triggered()), this, SLOT(slotXtSyncCurrentMailbox())); #endif m_replyPrivate = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_private"), this, SLOT(slotReplyTo()), this); m_replyPrivate->setEnabled(false); m_replyAllButMe = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all_but_me"), this, SLOT(slotReplyAllButMe()), this); m_replyAllButMe->setEnabled(false); m_replyAll = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_all"), this, SLOT(slotReplyAll()), this); m_replyAll->setEnabled(false); m_replyList = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_list"), this, SLOT(slotReplyList()), this); m_replyList->setEnabled(false); m_replyGuess = ShortcutHandler::instance()->createAction(QStringLiteral("action_reply_guess"), this, SLOT(slotReplyGuess()), this); m_replyGuess->setEnabled(true); actionThreadMsgList = new QAction(UiUtils::loadIcon(QStringLiteral("format-justify-right")), tr("Show Messages in &Threads"), this); actionThreadMsgList->setCheckable(true); // This action is enabled/disabled by model's capabilities actionThreadMsgList->setEnabled(false); if (m_settings->value(Common::SettingsNames::guiMsgListShowThreading).toBool()) { actionThreadMsgList->setChecked(true); // The actual threading will be performed only when model updates its capabilities } connect(actionThreadMsgList, &QAction::triggered, this, &MainWindow::slotThreadMsgList); QActionGroup *sortOrderGroup = new QActionGroup(this); m_actionSortAscending = new QAction(tr("&Ascending"), sortOrderGroup); m_actionSortAscending->setCheckable(true); m_actionSortAscending->setChecked(true); m_actionSortDescending = new QAction(tr("&Descending"), sortOrderGroup); m_actionSortDescending->setCheckable(true); // QActionGroup has no toggle signal, but connecting descending will implicitly catch the acscending complement ;-) connect(m_actionSortDescending, &QAction::toggled, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_actionSortDescending, &QAction::toggled, this, &MainWindow::slotScrollToCurrent); connect(sortOrderGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged); QActionGroup *sortColumnGroup = new QActionGroup(this); m_actionSortNone = new QAction(tr("&No sorting"), sortColumnGroup); m_actionSortNone->setCheckable(true); m_actionSortThreading = new QAction(tr("Sorted by &Threading"), sortColumnGroup); m_actionSortThreading->setCheckable(true); m_actionSortByArrival = new QAction(tr("A&rrival"), sortColumnGroup); m_actionSortByArrival->setCheckable(true); m_actionSortByCc = new QAction(tr("&Cc (Carbon Copy)"), sortColumnGroup); m_actionSortByCc->setCheckable(true); m_actionSortByDate = new QAction(tr("Date from &Message Headers"), sortColumnGroup); m_actionSortByDate->setCheckable(true); m_actionSortByFrom = new QAction(tr("&From Address"), sortColumnGroup); m_actionSortByFrom->setCheckable(true); m_actionSortBySize = new QAction(tr("&Size"), sortColumnGroup); m_actionSortBySize->setCheckable(true); m_actionSortBySubject = new QAction(tr("S&ubject"), sortColumnGroup); m_actionSortBySubject->setCheckable(true); m_actionSortByTo = new QAction(tr("T&o Address"), sortColumnGroup); m_actionSortByTo->setCheckable(true); connect(sortColumnGroup, &QActionGroup::triggered, this, &MainWindow::slotSortingPreferenceChanged); slotSortingConfirmed(-1, Qt::AscendingOrder); actionHideRead = new QAction(tr("&Hide Read Messages"), this); actionHideRead->setCheckable(true); if (m_settings->value(Common::SettingsNames::guiMsgListHideRead).toBool()) { actionHideRead->setChecked(true); prettyMsgListModel->setHideRead(true); } connect(actionHideRead, &QAction::triggered, this, &MainWindow::slotHideRead); QActionGroup *layoutGroup = new QActionGroup(this); m_actionLayoutCompact = new QAction(tr("&Compact"), layoutGroup); m_actionLayoutCompact->setCheckable(true); m_actionLayoutCompact->setChecked(true); connect(m_actionLayoutCompact, &QAction::triggered, this, &MainWindow::slotLayoutCompact); m_actionLayoutWide = new QAction(tr("&Wide"), layoutGroup); m_actionLayoutWide->setCheckable(true); connect(m_actionLayoutWide, &QAction::triggered, this, &MainWindow::slotLayoutWide); m_actionLayoutOneAtTime = new QAction(tr("&One At Time"), layoutGroup); m_actionLayoutOneAtTime->setCheckable(true); connect(m_actionLayoutOneAtTime, &QAction::triggered, this, &MainWindow::slotLayoutOneAtTime); m_actionShowOnlySubscribed = new QAction(tr("Show Only S&ubscribed Folders"), this); m_actionShowOnlySubscribed->setCheckable(true); m_actionShowOnlySubscribed->setEnabled(false); connect(m_actionShowOnlySubscribed, &QAction::toggled, this, &MainWindow::slotShowOnlySubscribed); m_actionSubscribeMailbox = new QAction(tr("Su&bscribed"), this); m_actionSubscribeMailbox->setCheckable(true); m_actionSubscribeMailbox->setEnabled(false); connect(m_actionSubscribeMailbox, &QAction::triggered, this, &MainWindow::slotSubscribeCurrentMailbox); - aboutTrojita = new QAction(trUtf8("&About Trojitá..."), this); + aboutTrojita = new QAction(tr("&About Trojitá..."), this); connect(aboutTrojita, &QAction::triggered, this, &MainWindow::slotShowAboutTrojita); donateToTrojita = new QAction(tr("&Donate to the project"), this); connect(donateToTrojita, &QAction::triggered, this, &MainWindow::slotDonateToTrojita); connectModelActions(); m_composeMenu = new QMenu(tr("Compose Mail"), this); m_composeMenu->addAction(composeMail); m_composeMenu->addAction(m_editDraft); m_composeButton = new QToolButton(this); m_composeButton->setPopupMode(QToolButton::MenuButtonPopup); m_composeButton->setMenu(m_composeMenu); m_composeButton->setDefaultAction(composeMail); m_replyButton = new QToolButton(this); m_replyButton->setPopupMode(QToolButton::MenuButtonPopup); m_replyMenu = new QMenu(m_replyButton); m_replyMenu->addAction(m_replyPrivate); m_replyMenu->addAction(m_replyAllButMe); m_replyMenu->addAction(m_replyAll); m_replyMenu->addAction(m_replyList); m_replyButton->setMenu(m_replyMenu); m_replyButton->setDefaultAction(m_replyPrivate); m_mainToolbar->addWidget(m_composeButton); m_mainToolbar->addWidget(m_replyButton); m_mainToolbar->addAction(m_forwardAsAttachment); m_mainToolbar->addAction(expunge); m_mainToolbar->addSeparator(); m_mainToolbar->addAction(markAsRead); m_mainToolbar->addAction(markAsDeleted); m_mainToolbar->addAction(markAsFlagged); m_mainToolbar->addAction(markAsJunk); m_mainToolbar->addAction(markAsNotJunk); m_mainToolbar->addAction(moveToArchive); // Push the status indicators all the way to the other side of the toolbar -- either to the far right, or far bottom. QWidget *toolbarSpacer = new QWidget(m_mainToolbar); toolbarSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); m_mainToolbar->addWidget(toolbarSpacer); m_mainToolbar->addSeparator(); m_mainToolbar->addWidget(busyParsersIndicator); networkIndicator = new QToolButton(this); // This is deliberate; we want to show this button in the same style as the other ones in the toolbar networkIndicator->setPopupMode(QToolButton::MenuButtonPopup); m_mainToolbar->addWidget(networkIndicator); m_menuFromToolBar = new QToolButton(this); m_menuFromToolBar->setIcon(UiUtils::loadIcon(QStringLiteral("menu_new"))); m_menuFromToolBar->setText(QChar(0x205d)); // Unicode 'TRICOLON' m_menuFromToolBar->setPopupMode(QToolButton::MenuButtonPopup); connect(m_menuFromToolBar, &QAbstractButton::clicked, m_menuFromToolBar, &QToolButton::showMenu); m_mainToolbar->addWidget(m_menuFromToolBar); connect(showMenuBar, &QAction::toggled, [this](const bool menuBarVisible) { // https://bugreports.qt.io/browse/QTBUG-35768 , we have to work on the QAction, not QToolButton m_mainToolbar->actions().last()->setVisible(!menuBarVisible); }); busyParsersIndicator->setFixedSize(m_mainToolbar->iconSize()); { // Custom widgets which are added into a QToolBar are by default aligned to the left, while QActions are justified. // That sucks, because some of our widgets use multiple actions with an expanding arrow at right. // Make sure everything is aligned to the left, so that the actual buttons are aligned properly and the extra arrows // are, well, at right. // I have no idea how this works on RTL layouts. QLayout *lay = m_mainToolbar->layout(); for (int i = 0; i < lay->count(); ++i) { QLayoutItem *it = lay->itemAt(i); if (it->widget() == toolbarSpacer) { // Don't align this one, otherwise it won't push stuff when in horizontal direction continue; } if (it->widget() == busyParsersIndicator) { // It looks much better when centered it->setAlignment(Qt::AlignJustify); continue; } it->setAlignment(Qt::AlignLeft); } } updateMessageFlags(); } void MainWindow::connectModelActions() { connect(reloadAllMailboxes, &QAction::triggered, imapModel(), &Imap::Mailbox::Model::reloadMailboxList); connect(netOffline, &QAction::triggered, qobject_cast(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOffline); connect(netExpensive, &QAction::triggered, qobject_cast(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkExpensive); connect(netOnline, &QAction::triggered, qobject_cast(m_imapAccess->networkWatcher()), &Imap::Mailbox::NetworkWatcher::setNetworkOnline); netExpensive->setEnabled(imapAccess()->isConfigured()); netOnline->setEnabled(imapAccess()->isConfigured()); } void MainWindow::createMenus() { #define ADD_ACTION(MENU, ACTION) \ MENU->addAction(ACTION); \ addAction(ACTION); QMenu *applicationMenu = menuBar()->addMenu(tr("&Application")); ADD_ACTION(applicationMenu, m_actionContactEditor); QMenu *netPolicyMenu = applicationMenu->addMenu(tr("&Network Access")); ADD_ACTION(netPolicyMenu, netOffline); ADD_ACTION(netPolicyMenu, netExpensive); ADD_ACTION(netPolicyMenu, netOnline); QMenu *debugMenu = applicationMenu->addMenu(tr("&Debugging")); ADD_ACTION(debugMenu, showFullView); ADD_ACTION(debugMenu, showTaskView); ADD_ACTION(debugMenu, showMimeView); ADD_ACTION(debugMenu, showProtocolLogger); ADD_ACTION(debugMenu, logPersistent); debugMenu->addSeparator(); ADD_ACTION(debugMenu, showImapCapabilities); debugMenu->addSeparator(); ADD_ACTION(debugMenu, reloadAllMailboxes); ADD_ACTION(debugMenu, resyncMbox); applicationMenu->addSeparator(); ADD_ACTION(applicationMenu, configSettings); ADD_ACTION(applicationMenu, ShortcutHandler::instance()->shortcutConfigAction()); applicationMenu->addSeparator(); ADD_ACTION(applicationMenu, aboutTrojita); ADD_ACTION(applicationMenu, donateToTrojita); applicationMenu->addSeparator(); ADD_ACTION(applicationMenu, exitAction); QMenu *viewMenu = menuBar()->addMenu(tr("&View")); ADD_ACTION(viewMenu, showMenuBar); ADD_ACTION(viewMenu, showToolBar); QMenu *layoutMenu = viewMenu->addMenu(tr("&Layout")); ADD_ACTION(layoutMenu, m_actionLayoutCompact); ADD_ACTION(layoutMenu, m_actionLayoutWide); ADD_ACTION(layoutMenu, m_actionLayoutOneAtTime); viewMenu->addSeparator(); ADD_ACTION(viewMenu, m_actionShowOnlySubscribed); QMenu *mailboxMenu = menuBar()->addMenu(tr("Mail&box")); QMenu *sortMenu = mailboxMenu->addMenu(tr("S&orting")); ADD_ACTION(sortMenu, m_actionSortNone); ADD_ACTION(sortMenu, m_actionSortThreading); ADD_ACTION(sortMenu, m_actionSortByArrival); ADD_ACTION(sortMenu, m_actionSortByCc); ADD_ACTION(sortMenu, m_actionSortByDate); ADD_ACTION(sortMenu, m_actionSortByFrom); ADD_ACTION(sortMenu, m_actionSortBySize); ADD_ACTION(sortMenu, m_actionSortBySubject); ADD_ACTION(sortMenu, m_actionSortByTo); sortMenu->addSeparator(); ADD_ACTION(sortMenu, m_actionSortAscending); ADD_ACTION(sortMenu, m_actionSortDescending); sortMenu->addSeparator(); ADD_ACTION(sortMenu, actionThreadMsgList); ADD_ACTION(sortMenu, actionHideRead); mailboxMenu->addSeparator(); ADD_ACTION(mailboxMenu, m_previousMessage); ADD_ACTION(mailboxMenu, m_nextMessage); mailboxMenu->addSeparator(); ADD_ACTION(mailboxMenu, expunge); QMenu *messageMenu = menuBar()->addMenu(tr("&Message")); ADD_ACTION(messageMenu, composeMail); ADD_ACTION(messageMenu, m_editDraft); messageMenu->addSeparator(); ADD_ACTION(messageMenu, m_replyGuess); ADD_ACTION(messageMenu, m_replyPrivate); ADD_ACTION(messageMenu, m_replyAll); ADD_ACTION(messageMenu, m_replyAllButMe); ADD_ACTION(messageMenu, m_replyList); messageMenu->addSeparator(); ADD_ACTION(messageMenu, m_forwardAsAttachment); ADD_ACTION(messageMenu, m_resend); QMenu *mainMenuBehindToolBar = new QMenu(this); m_menuFromToolBar->setMenu(mainMenuBehindToolBar); m_menuFromToolBar->menu()->addMenu(applicationMenu); m_menuFromToolBar->menu()->addMenu(viewMenu); m_menuFromToolBar->menu()->addMenu(mailboxMenu); m_menuFromToolBar->menu()->addMenu(messageMenu); m_menuFromToolBar->menu()->addSeparator(); m_menuFromToolBar->menu()->addAction(showMenuBar); networkIndicator->setMenu(netPolicyMenu); m_netToolbarDefaultAction = new QAction(this); networkIndicator->setDefaultAction(m_netToolbarDefaultAction); connect(m_netToolbarDefaultAction, &QAction::triggered, networkIndicator, &QToolButton::showMenu); connect(netOffline, &QAction::toggled, this, &MainWindow::updateNetworkIndication); connect(netExpensive, &QAction::toggled, this, &MainWindow::updateNetworkIndication); connect(netOnline, &QAction::toggled, this, &MainWindow::updateNetworkIndication); addToolBar(Qt::LeftToolBarArea, m_mainToolbar); m_mainToolbar->actions().last()->setVisible(true); // initial state to complement the default of the QMenuBar's visibility menuBar()->hide(); #undef ADD_ACTION } void MainWindow::createWidgets() { // The state of the GUI is only saved after a certain time has passed. This is just an optimization to make sure // we do not hit the disk continually when e.g. resizing some random widget. m_delayedStateSaving = new QTimer(this); m_delayedStateSaving->setInterval(1000); m_delayedStateSaving->setSingleShot(true); connect(m_delayedStateSaving, &QTimer::timeout, this, &MainWindow::saveSizesAndState); mboxTree = new MailBoxTreeView(nullptr, m_settings); mboxTree->setDesiredExpansion(m_settings->value(Common::SettingsNames::guiExpandedMailboxes).toStringList()); connect(mboxTree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMboxTree); connect(mboxTree, &MailBoxTreeView::mailboxExpansionChanged, this, [this](const QStringList &mailboxNames) { m_settings->setValue(Common::SettingsNames::guiExpandedMailboxes, mailboxNames); }); msgListWidget = new MessageListWidget(nullptr, m_favoriteTags); msgListWidget->tree->setContextMenuPolicy(Qt::CustomContextMenu); msgListWidget->tree->setAlternatingRowColors(true); msgListWidget->setRawSearchEnabled(m_settings->value(Common::SettingsNames::guiAllowRawSearch).toBool()); connect (msgListWidget, &MessageListWidget::rawSearchSettingChanged, this, &MainWindow::saveRawStateSetting); connect(msgListWidget->tree, &QWidget::customContextMenuRequested, this, &MainWindow::showContextMenuMsgListTree); connect(msgListWidget->tree, &QAbstractItemView::activated, this, &MainWindow::msgListClicked); connect(msgListWidget->tree, &QAbstractItemView::clicked, this, &MainWindow::msgListClicked); connect(msgListWidget->tree, &QAbstractItemView::doubleClicked, this, &MainWindow::openCompleteMessageWidget); connect(msgListWidget, &MessageListWidget::requestingSearch, this, &MainWindow::slotSearchRequested); connect(msgListWidget->tree->header(), &QHeaderView::sectionMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(msgListWidget->tree->header(), &QHeaderView::sectionResized, m_delayedStateSaving, static_cast(&QTimer::start)); msgListWidget->tree->installEventFilter(this); m_messageWidget = new CompleteMessageWidget(this, m_settings, m_pluginManager, m_favoriteTags); connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::scrollMessageUp); connect(m_messageWidget->messageView, &MessageView::messageChanged, this, &MainWindow::slotUpdateMessageActions); #if QT_VERSION >= QT_VERSION_CHECK(5, 4, 0) connect(m_messageWidget->messageView, &MessageView::linkHovered, [](const QString &url) { if (url.isEmpty()) { QToolTip::hideText(); } else { // indirection due to https://bugs.kde.org/show_bug.cgi?id=363783 QTimer::singleShot(250, [url]() { QToolTip::showText(QCursor::pos(), QObject::tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(url))); }); } }); #endif connect(m_messageWidget->messageView, &MessageView::transferError, this, &MainWindow::slotDownloadTransferError); // Do not try to get onto the homepage when we are on EXPENSIVE connection if (m_settings->value(Common::SettingsNames::appLoadHomepage, QVariant(true)).toBool() && m_imapAccess->preferredNetworkPolicy() == Imap::Mailbox::NETWORK_ONLINE) { m_messageWidget->messageView->setHomepageUrl(QUrl(QStringLiteral("http://welcome.trojita.flaska.net/%1").arg(Common::Application::version))); } allDock = new QDockWidget(tr("Everything"), this); allDock->setObjectName(QStringLiteral("allDock")); allTree = new QTreeView(allDock); allDock->hide(); allTree->setUniformRowHeights(true); allTree->setHeaderHidden(true); allDock->setWidget(allTree); addDockWidget(Qt::LeftDockWidgetArea, allDock); taskDock = new QDockWidget(tr("IMAP Tasks"), this); taskDock->setObjectName(QStringLiteral("taskDock")); taskTree = new QTreeView(taskDock); taskDock->hide(); taskTree->setHeaderHidden(true); taskDock->setWidget(taskTree); addDockWidget(Qt::LeftDockWidgetArea, taskDock); mailMimeDock = new QDockWidget(tr("MIME Tree"), this); mailMimeDock->setObjectName(QStringLiteral("mailMimeDock")); mailMimeTree = new QTreeView(mailMimeDock); mailMimeDock->hide(); mailMimeTree->setUniformRowHeights(true); mailMimeTree->setHeaderHidden(true); mailMimeDock->setWidget(mailMimeTree); addDockWidget(Qt::RightDockWidgetArea, mailMimeDock); connect(m_messageWidget->messageView, &MessageView::messageModelChanged, this, &MainWindow::slotMessageModelChanged); protocolLoggerDock = new QDockWidget(tr("Protocol log"), this); protocolLoggerDock->setObjectName(QStringLiteral("protocolLoggerDock")); protocolLogger = new ProtocolLoggerWidget(protocolLoggerDock); protocolLoggerDock->hide(); protocolLoggerDock->setWidget(protocolLogger); addDockWidget(Qt::BottomDockWidgetArea, protocolLoggerDock); busyParsersIndicator = new TaskProgressIndicator(this); } void MainWindow::setupModels() { m_imapAccess->reloadConfiguration(); m_imapAccess->doConnect(); m_messageWidget->messageView->setNetworkWatcher(qobject_cast(m_imapAccess->networkWatcher())); auto realThreadingModel = qobject_cast(m_imapAccess->threadingMsgListModel()); Q_ASSERT(realThreadingModel); auto realMsgListModel = qobject_cast(m_imapAccess->msgListModel()); Q_ASSERT(realMsgListModel); prettyMboxModel = new Imap::Mailbox::PrettyMailboxModel(this, qobject_cast(m_imapAccess->mailboxModel())); prettyMboxModel->setObjectName(QStringLiteral("prettyMboxModel")); connect(realThreadingModel, &Imap::Mailbox::ThreadingMsgListModel::sortingFailed, msgListWidget, &MessageListWidget::slotSortingFailed); prettyMsgListModel = new Imap::Mailbox::PrettyMsgListModel(this); prettyMsgListModel->setSourceModel(m_imapAccess->threadingMsgListModel()); prettyMsgListModel->setObjectName(QStringLiteral("prettyMsgListModel")); connect(mboxTree, &MailBoxTreeView::clicked, realMsgListModel, static_cast(&Imap::Mailbox::MsgListModel::setMailbox)); connect(mboxTree, &MailBoxTreeView::activated, realMsgListModel, static_cast(&Imap::Mailbox::MsgListModel::setMailbox)); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::dataChanged, this, &MainWindow::updateMessageFlags); connect(qobject_cast(m_imapAccess->msgListModel()), &Imap::Mailbox::MsgListModel::messagesAvailable, this, &MainWindow::slotScrollToUnseenMessage); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsInserted, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::rowsRemoved, this, &MainWindow::updateMessageFlags); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::layoutChanged, this, &MainWindow::updateMessageFlags); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, msgListWidget, &MessageListWidget::slotAutoEnableDisableSearch); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::updateMessageFlags); connect(realMsgListModel, &Imap::Mailbox::MsgListModel::mailboxChanged, this, &MainWindow::slotMailboxChanged); connect(imapModel(), &Imap::Mailbox::Model::alertReceived, this, &MainWindow::alertReceived); connect(imapModel(), &Imap::Mailbox::Model::imapError, this, &MainWindow::imapError); connect(imapModel(), &Imap::Mailbox::Model::networkError, this, &MainWindow::networkError); connect(imapModel(), &Imap::Mailbox::Model::authRequested, this, &MainWindow::authenticationRequested, Qt::QueuedConnection); connect(imapModel(), &Imap::Mailbox::Model::authAttemptFailed, this, [this]() { m_ignoreStoredPassword = true; }); connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOffline, this, &MainWindow::networkPolicyOffline); connect(imapModel(), &Imap::Mailbox::Model::networkPolicyExpensive, this, &MainWindow::networkPolicyExpensive); connect(imapModel(), &Imap::Mailbox::Model::networkPolicyOnline, this, &MainWindow::networkPolicyOnline); connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, this, [this](uint, const Imap::ConnectionState state) { if (state == Imap::CONN_STATE_AUTHENTICATED) { m_ignoreStoredPassword = false; } }); connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, this, &MainWindow::showConnectionStatus); connect(imapModel(), &Imap::Mailbox::Model::mailboxDeletionFailed, this, &MainWindow::slotMailboxDeleteFailed); connect(imapModel(), &Imap::Mailbox::Model::mailboxCreationFailed, this, &MainWindow::slotMailboxCreateFailed); connect(imapModel(), &Imap::Mailbox::Model::mailboxSyncFailed, this, &MainWindow::slotMailboxSyncFailed); connect(imapModel(), &Imap::Mailbox::Model::logged, protocolLogger, &ProtocolLoggerWidget::log); connect(imapModel(), &Imap::Mailbox::Model::connectionStateChanged, protocolLogger, &ProtocolLoggerWidget::onConnectionClosed); auto nw = qobject_cast(m_imapAccess->networkWatcher()); Q_ASSERT(nw); connect(nw, &Imap::Mailbox::NetworkWatcher::reconnectAttemptScheduled, this, [this](const int timeout) { showStatusMessage(tr("Attempting to reconnect in %n seconds..", 0, timeout/1000)); }); connect(nw, &Imap::Mailbox::NetworkWatcher::resetReconnectState, this, &MainWindow::slotResetReconnectState); connect(imapModel(), &Imap::Mailbox::Model::mailboxFirstUnseenMessage, this, &MainWindow::slotScrollToUnseenMessage); connect(imapModel(), &Imap::Mailbox::Model::capabilitiesUpdated, this, &MainWindow::slotCapabilitiesUpdated); connect(m_imapAccess->msgListModel(), &QAbstractItemModel::modelReset, this, &MainWindow::slotUpdateWindowTitle); connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::slotUpdateWindowTitle); connect(prettyMsgListModel, &Imap::Mailbox::PrettyMsgListModel::sortingPreferenceChanged, this, &MainWindow::slotSortingConfirmed); //Imap::Mailbox::ModelWatcher* w = new Imap::Mailbox::ModelWatcher( this ); //w->setModel( imapModel() ); //ModelTest* tester = new ModelTest( prettyMboxModel, this ); // when testing, test just one model at time mboxTree->setModel(prettyMboxModel); msgListWidget->tree->setModel(prettyMsgListModel); connect(msgListWidget->tree->selectionModel(), &QItemSelectionModel::selectionChanged, this, &MainWindow::updateMessageFlags); allTree->setModel(imapModel()); taskTree->setModel(imapModel()->taskModel()); connect(imapModel()->taskModel(), &QAbstractItemModel::layoutChanged, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::modelReset, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::rowsInserted, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::rowsRemoved, taskTree, &QTreeView::expandAll); connect(imapModel()->taskModel(), &QAbstractItemModel::rowsMoved, taskTree, &QTreeView::expandAll); busyParsersIndicator->setImapModel(imapModel()); auto accountIconName = m_settings->value(Common::SettingsNames::imapAccountIcon).toString(); if (accountIconName.isEmpty()) { qApp->setWindowIcon(UiUtils::loadIcon(QStringLiteral("trojita"))); } else if (accountIconName.contains(QDir::separator())) { // Absolute paths are OK for users, but unsupported by our icon loader qApp->setWindowIcon(QIcon(accountIconName)); } else { qApp->setWindowIcon(UiUtils::loadIcon(accountIconName)); } } void MainWindow::createSysTray() { if (m_trayIcon) return; qApp->setQuitOnLastWindowClosed(false); m_trayIcon = new QSystemTrayIcon(this); handleTrayIconChange(); QAction* quitAction = new QAction(tr("&Quit"), m_trayIcon); connect(quitAction, &QAction::triggered, qApp, &QApplication::quit); QMenu *trayIconMenu = new QMenu(this); trayIconMenu->addAction(quitAction); m_trayIcon->setContextMenu(trayIconMenu); // QMenu cannot be a child of QSystemTrayIcon, and we don't want the QMenu in MainWindow scope. connect(m_trayIcon, &QObject::destroyed, trayIconMenu, &QObject::deleteLater); connect(m_trayIcon, &QSystemTrayIcon::activated, this, &MainWindow::slotIconActivated); connect(imapModel(), &Imap::Mailbox::Model::messageCountPossiblyChanged, this, &MainWindow::handleTrayIconChange); m_trayIcon->setVisible(true); m_trayIcon->show(); } void MainWindow::removeSysTray() { delete m_trayIcon; m_trayIcon = 0; qApp->setQuitOnLastWindowClosed(true); } void MainWindow::slotToggleSysTray() { bool showSystray = m_settings->value(Common::SettingsNames::guiShowSystray, QVariant(true)).toBool(); if (showSystray && !m_trayIcon && QSystemTrayIcon::isSystemTrayAvailable()) { createSysTray(); } else if (!showSystray && m_trayIcon) { removeSysTray(); } } void MainWindow::handleTrayIconChange() { if (!m_trayIcon) return; const bool isOffline = qobject_cast(m_imapAccess->networkWatcher())->effectiveNetworkPolicy() == Imap::Mailbox::NETWORK_OFFLINE; auto pixmap = qApp->windowIcon() .pixmap(QSize(32, 32), isOffline ? QIcon::Disabled : QIcon::Normal); QString tooltip; auto profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); if (profileName.isEmpty()) { tooltip = QStringLiteral("Trojitá"); } else { tooltip = QStringLiteral("Trojitá [%1]").arg(profileName); } int unreadCount = 0; bool numbersValid = false; auto watchingMode = settings()->value(Common::SettingsNames::watchedFoldersKey).toString(); if (watchingMode == Common::SettingsNames::watchAll || watchingMode == Common::SettingsNames::watchSubscribed) { bool subscribedOnly = watchingMode == Common::SettingsNames::watchSubscribed; unreadCount = std::accumulate(UiUtils::QaimDfsIterator(m_imapAccess->mailboxModel()->index(0, 0), m_imapAccess->mailboxModel()), UiUtils::QaimDfsIterator(), 0, [subscribedOnly](const int acc, const QModelIndex &idx) { if (subscribedOnly && !idx.data(Imap::Mailbox::RoleMailboxIsSubscribed).toBool()) return acc; auto x = idx.data(Imap::Mailbox::RoleUnreadMessageCount).toInt(); if (x > 0) { return acc + x; } else { return acc; } }); // only show stuff if there are some mailboxes, and if there are such messages numbersValid = m_imapAccess->mailboxModel()->hasChildren() && unreadCount > 0; } else { // just for the INBOX QModelIndex mailbox = imapModel()->index(1, 0, QModelIndex()); if (mailbox.isValid() && mailbox.data(Imap::Mailbox::RoleMailboxName).toString() == QLatin1String("INBOX") && mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt() > 0) { unreadCount = mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt(); numbersValid = true; } } if (numbersValid) { QFont f; f.setPixelSize(pixmap.height() * 0.59); f.setWeight(QFont::Bold); QString text = QString::number(unreadCount); QFontMetrics fm(f); if (unreadCount > 666) { // You just have too many messages. text = QStringLiteral("🐮"); fm = QFontMetrics(f); } else if (fm.width(text) > pixmap.width()) { f.setPixelSize(f.pixelSize() * pixmap.width() / fm.width(text)); fm = QFontMetrics(f); } QRect boundingRect = fm.tightBoundingRect(text); boundingRect.setWidth(boundingRect.width() + 2); boundingRect.setHeight(boundingRect.height() + 2); boundingRect.moveCenter(QPoint(pixmap.width() / 2, pixmap.height() / 2)); boundingRect = boundingRect.intersected(pixmap.rect()); QPainterPath path; path.addText(boundingRect.bottomLeft(), f, text); QPainter painter(&pixmap); painter.setRenderHint(QPainter::Antialiasing); painter.setPen(QColor(255,255,255, 180)); painter.setBrush(isOffline ? Qt::red : Qt::black); painter.drawPath(path); //: This is a tooltip for the tray icon. It will be prefixed by something like "Trojita" or "Trojita [work]" - tooltip += trUtf8(" - %n unread message(s)", 0, unreadCount); + tooltip += tr(" - %n unread message(s)", 0, unreadCount); } else if (isOffline) { //: A tooltip suffix when offline. The prefix is something like "Trojita" or "Trojita [work]" tooltip += tr(" - offline"); } m_trayIcon->setToolTip(tooltip); m_trayIcon->setIcon(QIcon(pixmap)); } void MainWindow::closeEvent(QCloseEvent *event) { if (m_trayIcon && m_trayIcon->isVisible()) { - Util::askForSomethingUnlessTold(trUtf8("Trojitá"), + Util::askForSomethingUnlessTold(tr("Trojitá"), tr("The application will continue in systray. This can be disabled within the settings."), Common::SettingsNames::guiOnSystrayClose, QMessageBox::Ok, this, m_settings); hide(); event->ignore(); } } bool MainWindow::eventFilter(QObject *o, QEvent *e) { if (msgListWidget && o == msgListWidget->tree && m_messageWidget->messageView) { if (e->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(e); if (keyEvent->key() == Qt::Key_Space || keyEvent->key() == Qt::Key_Backspace) { QCoreApplication::sendEvent(m_messageWidget, keyEvent); return true; } return false; } return false; } if (msgListWidget && msgListWidget->tree && o == msgListWidget->tree->header()->viewport()) { // installed if sorting is not really possible. QWidget *header = static_cast(o); QMouseEvent *mouse = static_cast(e); if (e->type() == QEvent::MouseButtonPress) { if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor) { m_headerDragStart = mouse->pos(); } return false; } if (e->type() == QEvent::MouseButtonRelease) { if (mouse->button() == Qt::LeftButton && header->cursor().shape() == Qt::ArrowCursor && (m_headerDragStart - mouse->pos()).manhattanLength() < QApplication::startDragDistance()) { m_actionSortDescending->toggle(); Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder; msgListWidget->tree->header()->setSortIndicator(-1, order); return true; // prevent regular click } } } return false; } void MainWindow::slotIconActivated(const QSystemTrayIcon::ActivationReason reason) { if (reason == QSystemTrayIcon::Trigger) { setVisible(!isVisible()); if (isVisible()) showMainWindow(); } } void MainWindow::showMainWindow() { setVisible(true); activateWindow(); raise(); } void MainWindow::msgListClicked(const QModelIndex &index) { Q_ASSERT(index.isValid()); if (qApp->keyboardModifiers() & Qt::ShiftModifier || qApp->keyboardModifiers() & Qt::ControlModifier) return; if (! index.data(Imap::Mailbox::RoleMessageUid).isValid()) return; // Because it's quite possible that we have switched into another mailbox, make sure that we're in the "current" one so that // user will be notified about new arrivals, etc. QModelIndex translated = Imap::deproxifiedIndex(index); imapModel()->switchToMailbox(translated.parent().parent()); if (index.column() == Imap::Mailbox::MsgListModel::SEEN) { if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool()) return; Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool() ? Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD; imapModel()->markMessagesRead(QModelIndexList() << translated, flagOp); if (translated == m_messageWidget->messageView->currentMessage()) { m_messageWidget->messageView->stopAutoMarkAsRead(); } } else if (index.column() == Imap::Mailbox::MsgListModel::FLAGGED) { if (!translated.data(Imap::Mailbox::RoleIsFetched).toBool()) return; Imap::Mailbox::FlagsOperation flagOp = translated.data(Imap::Mailbox::RoleMessageIsMarkedFlagged).toBool() ? Imap::Mailbox::FLAG_REMOVE : Imap::Mailbox::FLAG_ADD; imapModel()->setMessageFlags(QModelIndexList() << translated, Imap::Mailbox::FlagNames::flagged, flagOp); } else { if ((m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) || m_layoutMode == LAYOUT_ONE_AT_TIME) { // isVisible() won't work, the splitter manipulates width, not the visibility state m_messageWidget->messageView->setMessage(index); } msgListWidget->tree->setCurrentIndex(index); } } void MainWindow::openCompleteMessageWidget() { const QModelIndex index = msgListWidget->tree->currentIndex(); if (! index.data(Imap::Mailbox::RoleMessageUid).isValid()) return; CompleteMessageWidget *widget = new CompleteMessageWidget(0, m_settings, m_pluginManager, m_favoriteTags); widget->messageView->setMessage(index); widget->messageView->setNetworkWatcher(qobject_cast(m_imapAccess->networkWatcher())); widget->setFocusPolicy(Qt::StrongFocus); widget->setWindowTitle(index.data(Imap::Mailbox::RoleMessageSubject).toString()); widget->setAttribute(Qt::WA_DeleteOnClose); QAction *closeAction = ShortcutHandler::instance()->createAction(QStringLiteral("action_messagewindow_close"), widget, SLOT(close()), widget); widget->addAction(closeAction); widget->show(); } void MainWindow::showContextMenuMboxTree(const QPoint &position) { QList actionList; if (mboxTree->indexAt(position).isValid()) { actionList.append(createChildMailbox); actionList.append(deleteCurrentMailbox); actionList.append(m_actionMarkMailboxAsRead); actionList.append(resyncMbox); actionList.append(reloadMboxList); actionList.append(m_actionSubscribeMailbox); m_actionSubscribeMailbox->setChecked(mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxIsSubscribed).toBool()); #ifdef XTUPLE_CONNECT actionList.append(xtIncludeMailboxInSync); xtIncludeMailboxInSync->setChecked( m_settings->value(Common::SettingsNames::xtSyncMailboxList).toStringList().contains( mboxTree->indexAt(position).data(Imap::Mailbox::RoleMailboxName).toString())); #endif } else { actionList.append(createTopMailbox); } actionList.append(reloadAllMailboxes); actionList.append(m_actionShowOnlySubscribed); QMenu::exec(actionList, mboxTree->mapToGlobal(position), nullptr, this); } void MainWindow::showContextMenuMsgListTree(const QPoint &position) { QList actionList; QModelIndex index = msgListWidget->tree->indexAt(position); if (index.isValid()) { updateMessageFlagsOf(index); actionList.append(markAsRead); actionList.append(markAsDeleted); actionList.append(markAsFlagged); actionList.append(markAsJunk); actionList.append(markAsNotJunk); actionList.append(moveToArchive); actionList.append(m_actionMarkMailboxAsRead); actionList.append(saveWholeMessage); actionList.append(viewMsgSource); actionList.append(viewMsgHeaders); auto appendTagIfExists = [this,&actionList](const int row, QAction *tag) { if (m_favoriteTags->rowCount() > row - 1) actionList.append(tag); }; appendTagIfExists(1, tag1); appendTagIfExists(2, tag2); appendTagIfExists(3, tag3); appendTagIfExists(4, tag4); appendTagIfExists(5, tag5); appendTagIfExists(6, tag6); appendTagIfExists(7, tag7); appendTagIfExists(8, tag8); appendTagIfExists(9, tag9); } if (! actionList.isEmpty()) QMenu::exec(actionList, msgListWidget->tree->mapToGlobal(position), nullptr, this); } /** @short Ask for an updated list of mailboxes situated below the selected one */ void MainWindow::slotReloadMboxList() { Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; Imap::Mailbox::TreeItemMailbox *mbox = dynamic_cast( Imap::Mailbox::Model::realTreeItem(item) ); Q_ASSERT(mbox); mbox->rescanForChildMailboxes(imapModel()); } } /** @short Request a check for new messages in selected mailbox */ void MainWindow::slotResyncMbox() { if (! imapModel()->isNetworkAvailable()) return; Q_FOREACH(const QModelIndex &item, mboxTree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; imapModel()->resyncMailbox(item); } } void MainWindow::alertReceived(const QString &message) { //: "ALERT" is a special warning which we're required to show to the user Gui::Util::messageBoxWarning(this, tr("IMAP Alert"), message); } void MainWindow::imapError(const QString &message) { Gui::Util::messageBoxCritical(this, tr("IMAP Protocol Error"), message); // Show the IMAP logger -- maybe some user will take that as a hint that they shall include it in the bug report. // showProtocolLogger->setChecked(true); } void MainWindow::networkError(const QString &message) { const QString title = tr("Network Error"); if (!m_networkErrorMessageBox) { m_networkErrorMessageBox = new QMessageBox(QMessageBox::Critical, title, QString(), QMessageBox::Ok, this); } // User must be informed about a new (but not recurring) error if (message != m_networkErrorMessageBox->text()) { m_networkErrorMessageBox->setText(message); if (qApp->applicationState() == Qt::ApplicationActive) { m_networkErrorMessageBox->setProperty(netErrorUnseen, false); m_networkErrorMessageBox->show(); } else { m_networkErrorMessageBox->setProperty(netErrorUnseen, true); if (m_trayIcon && m_trayIcon->isVisible()) m_trayIcon->showMessage(title, message, QSystemTrayIcon::Warning, 3333); } } } void MainWindow::cacheError(const QString &message) { Gui::Util::messageBoxCritical(this, tr("IMAP Cache Error"), tr("The caching subsystem managing a cache of the data already " "downloaded from the IMAP server is having troubles. " "All caching will be disabled.\n\n%1").arg(message)); } void MainWindow::networkPolicyOffline() { netExpensive->setChecked(false); netOnline->setChecked(false); netOffline->setChecked(true); updateActionsOnlineOffline(false); showStatusMessage(tr("Offline")); handleTrayIconChange(); } void MainWindow::networkPolicyExpensive() { netOffline->setChecked(false); netOnline->setChecked(false); netExpensive->setChecked(true); updateActionsOnlineOffline(true); handleTrayIconChange(); } void MainWindow::networkPolicyOnline() { netOffline->setChecked(false); netExpensive->setChecked(false); netOnline->setChecked(true); updateActionsOnlineOffline(true); handleTrayIconChange(); } /** @short Deletes a network error message box instance upon resetting of reconnect state */ void MainWindow::slotResetReconnectState() { if (m_networkErrorMessageBox) { delete m_networkErrorMessageBox; m_networkErrorMessageBox = 0; } } void MainWindow::slotShowSettings() { SettingsDialog *dialog = new SettingsDialog(this, m_senderIdentities, m_favoriteTags, m_settings); if (dialog->exec() == QDialog::Accepted) { // FIXME: wipe cache in case we're moving between servers nukeModels(); setupModels(); connectModelActions(); // The systray is still connected to the old model -- got to make sure it's getting updated removeSysTray(); slotToggleSysTray(); } QString method = m_settings->value(Common::SettingsNames::imapMethodKey).toString(); if (method != Common::SettingsNames::methodTCP && method != Common::SettingsNames::methodSSL && method != Common::SettingsNames::methodProcess ) { Gui::Util::messageBoxCritical(this, tr("No Configuration"), - trUtf8("No IMAP account is configured. Trojitá cannot do much without one.")); + tr("No IMAP account is configured. Trojitá cannot do much without one.")); } applySizesAndState(); } void MainWindow::authenticationRequested() { Plugins::PasswordPlugin *password = pluginManager()->password(); if (password) { // FIXME: use another account-id at some point in future // Currently the accountName will be empty unless Trojita has been // called with a profile, and then the profile will be used as the // accountName. QString accountName = m_imapAccess->accountName(); if (accountName.isEmpty()) accountName = QStringLiteral("account-0"); Plugins::PasswordJob *job = password->requestPassword(accountName, QStringLiteral("imap")); if (job) { connect(job, &Plugins::PasswordJob::passwordAvailable, this, [this](const QString &password) { authenticationContinue(password); }); connect(job, &Plugins::PasswordJob::error, this, [this](const Plugins::PasswordJob::Error error, const QString &message) { if (error == Plugins::PasswordJob::Error::NoSuchPassword) { authenticationContinue(QString()); } else { authenticationContinue(QString(), tr("Failed to retrieve password from the store: %1").arg(message)); } }); job->setAutoDelete(true); job->start(); return; } } authenticationContinue(QString()); } void MainWindow::authenticationContinue(const QString &password, const QString &errorMessage) { const QString &user = m_settings->value(Common::SettingsNames::imapUserKey).toString(); QString pass = password; if (m_ignoreStoredPassword || pass.isEmpty()) { auto dialog = PasswordDialog::getPassword(this, tr("Authentication Required"), tr("

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

").arg( user.toHtmlEscaped(), m_settings->value(Common::SettingsNames::imapHostKey).toString().toHtmlEscaped() ), errorMessage + (errorMessage.isEmpty() ? QString() : QStringLiteral("\n\n")) + imapModel()->imapAuthError()); connect(dialog, &PasswordDialog::gotPassword, imapModel(), &Imap::Mailbox::Model::setImapPassword); connect(dialog, &PasswordDialog::rejected, imapModel(), &Imap::Mailbox::Model::unsetImapPassword); } else { imapModel()->setImapPassword(pass); } } void MainWindow::checkSslPolicy() { m_imapAccess->setSslPolicy(QMessageBox(static_cast(m_imapAccess->sslInfoIcon()), m_imapAccess->sslInfoTitle(), m_imapAccess->sslInfoMessage(), QMessageBox::Yes | QMessageBox::No, this).exec() == QMessageBox::Yes); } void MainWindow::nukeModels() { m_messageWidget->messageView->setEmpty(); mboxTree->setModel(0); msgListWidget->tree->setModel(0); allTree->setModel(0); taskTree->setModel(0); delete prettyMsgListModel; prettyMsgListModel = 0; delete prettyMboxModel; prettyMboxModel = 0; } void MainWindow::recoverDrafts() { QDir draftPath(Common::writablePath(Common::LOCATION_CACHE) + QLatin1String("Drafts/")); QStringList drafts(draftPath.entryList(QStringList() << QStringLiteral("*.draft"))); Q_FOREACH(const QString &draft, drafts) { ComposeWidget *w = ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, draftPath.filePath(draft)), this); // No need to further try creating widgets for drafts if a nullptr is being returned by ComposeWidget::warnIfMsaNotConfigured if (!w) break; } } void MainWindow::slotComposeMail() { ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createBlank(this), this); } void MainWindow::slotEditDraft() { QString path(Common::writablePath(Common::LOCATION_DATA) + tr("Drafts")); QDir().mkpath(path); path = QFileDialog::getOpenFileName(this, tr("Edit draft"), path, tr("Drafts") + QLatin1String(" (*.draft)")); if (!path.isNull()) { ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createDraft(this, path), this); } } QModelIndexList MainWindow::translatedSelection() const { QModelIndexList translatedIndexes; Q_FOREACH(const QModelIndex &index, msgListWidget->tree->selectedTree()) { translatedIndexes << Imap::deproxifiedIndex(index); } return translatedIndexes; } void MainWindow::handleMarkAsRead(bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsRead: no valid messages"; } else { imapModel()->markMessagesRead(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); if (translatedIndexes.contains(m_messageWidget->messageView->currentMessage())) { m_messageWidget->messageView->stopAutoMarkAsRead(); } } } void MainWindow::slotNextUnread() { QModelIndex current = msgListWidget->tree->currentIndex(); UiUtils::gotoNext(msgListWidget->tree->model(), current, [](const QModelIndex &idx) { return !idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool(); }, [this](const QModelIndex &idx) { Q_ASSERT(!idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool()); m_messageWidget->messageView->setMessage(idx); msgListWidget->tree->setCurrentIndex(idx); }, []() { // nothing to do }); } void MainWindow::slotPreviousUnread() { QModelIndex current = msgListWidget->tree->currentIndex(); UiUtils::gotoPrevious(msgListWidget->tree->model(), current, [](const QModelIndex &idx) { return !idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool(); }, [this](const QModelIndex &idx) { Q_ASSERT(!idx.data(Imap::Mailbox::RoleMessageIsMarkedRead).toBool()); m_messageWidget->messageView->setMessage(idx); msgListWidget->tree->setCurrentIndex(idx); }, []() { // nothing to do }); } void MainWindow::handleTag(const bool checked, const int index) { const QModelIndexList &translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleTag: no valid messages"; } else { const auto &tagName = m_favoriteTags->tagNameByIndex(index); if (!tagName.isEmpty()) imapModel()->setMessageFlags(translatedIndexes, tagName, checked ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsDeleted(bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsDeleted: no valid messages"; } else { imapModel()->markMessagesDeleted(translatedIndexes, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsFlagged(const bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsFlagged: no valid messages"; } else { imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::flagged, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsJunk(const bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsJunk: no valid messages"; } else { if (value) { imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, Imap::Mailbox::FLAG_REMOVE); } imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::handleMarkAsNotJunk(const bool value) { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMarkAsNotJunk: no valid messages"; } else { if (value) { imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::junk, Imap::Mailbox::FLAG_REMOVE); } imapModel()->setMessageFlags(translatedIndexes, Imap::Mailbox::FlagNames::notjunk, value ? Imap::Mailbox::FLAG_ADD : Imap::Mailbox::FLAG_REMOVE); } } void MainWindow::slotMoveToArchiveFailed(const QString &error) { // XXX disable busy cursor QMessageBox::critical(this, tr("Failed to archive"), error); } void MainWindow::handleMoveToArchive() { const QModelIndexList translatedIndexes = translatedSelection(); if (translatedIndexes.isEmpty()) { qDebug() << "Model::handleMoveToArchive: no valid messages"; } else { auto archiveFolderName = m_settings->value(Common::SettingsNames::imapArchiveFolderName).toString(); auto copyMoveMessagesTask = imapModel()->copyMoveMessages( archiveFolderName.isEmpty() ? Common::SettingsNames::imapDefaultArchiveFolderName : archiveFolderName, translatedIndexes, Imap::Mailbox::CopyMoveOperation::MOVE); connect(copyMoveMessagesTask, &Imap::Mailbox::ImapTask::failed, this, &MainWindow::slotMoveToArchiveFailed); } } void MainWindow::slotExpunge() { imapModel()->expungeMailbox(qobject_cast(m_imapAccess->msgListModel())->currentMailbox()); } void MainWindow::slotMarkCurrentMailboxRead() { imapModel()->markMailboxAsRead(mboxTree->currentIndex()); } void MainWindow::slotCreateMailboxBelowCurrent() { createMailboxBelow(mboxTree->currentIndex()); } void MainWindow::slotCreateTopMailbox() { createMailboxBelow(QModelIndex()); } void MainWindow::createMailboxBelow(const QModelIndex &index) { Imap::Mailbox::TreeItemMailbox *mboxPtr = index.isValid() ? dynamic_cast( Imap::Mailbox::Model::realTreeItem(index)) : 0; Ui::CreateMailboxDialog ui; QDialog *dialog = new QDialog(this); ui.setupUi(dialog); dialog->setWindowTitle(mboxPtr ? tr("Create a Subfolder of %1").arg(mboxPtr->mailbox()) : tr("Create a Top-level Mailbox")); if (dialog->exec() == QDialog::Accepted) { QStringList parts; if (mboxPtr) parts << mboxPtr->mailbox(); parts << ui.mailboxName->text(); if (ui.otherMailboxes->isChecked()) parts << QString(); QString targetName = parts.join(mboxPtr ? mboxPtr->separator() : QString()); // FIXME: top-level separator imapModel()->createMailbox(targetName, ui.subscribe->isChecked() ? Imap::Mailbox::AutoSubscription::SUBSCRIBE : Imap::Mailbox::AutoSubscription::NO_EXPLICIT_SUBSCRIPTION ); } } void MainWindow::slotDeleteCurrentMailbox() { if (! mboxTree->currentIndex().isValid()) return; QModelIndex mailbox = Imap::deproxifiedIndex(mboxTree->currentIndex()); Q_ASSERT(mailbox.isValid()); QString name = mailbox.data(Imap::Mailbox::RoleMailboxName).toString(); if (QMessageBox::question(this, tr("Delete Mailbox"), tr("Are you sure to delete mailbox %1?").arg(name), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { imapModel()->deleteMailbox(name); } } void MainWindow::updateMessageFlags() { updateMessageFlagsOf(QModelIndex()); } void MainWindow::updateMessageFlagsOf(const QModelIndex &index) { QModelIndexList indexes = index.isValid() ? QModelIndexList() << index : translatedSelection(); const bool isValid = !indexes.isEmpty() && // either we operate on the -already valided- selection or the index must be valid (!index.isValid() || index.data(Imap::Mailbox::RoleMessageUid).toUInt() > 0); const bool okToModify = imapModel()->isNetworkAvailable() && isValid; markAsRead->setEnabled(okToModify); markAsDeleted->setEnabled(okToModify); markAsFlagged->setEnabled(okToModify); markAsJunk->setEnabled(okToModify); markAsNotJunk->setEnabled(okToModify); // There's no point in moving from Archive to, well, Archive auto archiveFolderName = m_settings->value(Common::SettingsNames::imapArchiveFolderName).toString(); if (archiveFolderName.isEmpty()) { archiveFolderName = Common::SettingsNames::imapDefaultArchiveFolderName; } moveToArchive->setEnabled(okToModify && std::any_of(indexes.cbegin(), indexes.cend(), [archiveFolderName](const QModelIndex &i) { return i.data(Imap::Mailbox::RoleMailboxName) != archiveFolderName; })); tag1->setEnabled(okToModify); tag2->setEnabled(okToModify); tag3->setEnabled(okToModify); tag4->setEnabled(okToModify); tag5->setEnabled(okToModify); tag6->setEnabled(okToModify); tag7->setEnabled(okToModify); tag8->setEnabled(okToModify); tag9->setEnabled(okToModify); bool isRead = isValid, isDeleted = isValid, isFlagged = isValid, isJunk = isValid, isNotJunk = isValid, hasTag1 = isValid, hasTag2 = isValid, hasTag3 = isValid, hasTag4 = isValid, hasTag5 = isValid, hasTag6 = isValid, hasTag7 = isValid, hasTag8 = isValid, hasTag9 = isValid; auto updateTag = [=](const QModelIndex &i, bool &hasTag, int index) { if (hasTag && !m_favoriteTags->tagNameByIndex(index).isEmpty() && !i.data(Imap::Mailbox::RoleMessageFlags).toStringList().contains(m_favoriteTags->tagNameByIndex(index))) { hasTag = false; } }; Q_FOREACH (const QModelIndex &i, indexes) { #define UPDATE_STATE(PROP) \ if (is##PROP && !i.data(Imap::Mailbox::RoleMessageIsMarked##PROP).toBool()) \ is##PROP = false; UPDATE_STATE(Read) UPDATE_STATE(Deleted) UPDATE_STATE(Flagged) UPDATE_STATE(Junk) UPDATE_STATE(NotJunk) #undef UPDATE_STATE updateTag(i, hasTag1, 0); updateTag(i, hasTag2, 1); updateTag(i, hasTag3, 2); updateTag(i, hasTag4, 3); updateTag(i, hasTag5, 4); updateTag(i, hasTag6, 5); updateTag(i, hasTag7, 6); updateTag(i, hasTag8, 7); updateTag(i, hasTag9, 8); } markAsRead->setChecked(isRead); markAsDeleted->setChecked(isDeleted); markAsFlagged->setChecked(isFlagged); markAsJunk->setChecked(isJunk && !isNotJunk); markAsNotJunk->setChecked(isNotJunk && !isJunk); tag1->setChecked(hasTag1); tag2->setChecked(hasTag2); tag3->setChecked(hasTag3); tag4->setChecked(hasTag4); tag5->setChecked(hasTag5); tag6->setChecked(hasTag6); tag7->setChecked(hasTag7); tag8->setChecked(hasTag8); tag9->setChecked(hasTag9); } void MainWindow::updateActionsOnlineOffline(bool online) { reloadMboxList->setEnabled(online); resyncMbox->setEnabled(online); expunge->setEnabled(online); createChildMailbox->setEnabled(online); createTopMailbox->setEnabled(online); deleteCurrentMailbox->setEnabled(online); m_actionMarkMailboxAsRead->setEnabled(online); updateMessageFlags(); showImapCapabilities->setEnabled(online); if (!online) { m_replyGuess->setEnabled(false); m_replyPrivate->setEnabled(false); m_replyAll->setEnabled(false); m_replyAllButMe->setEnabled(false); m_replyList->setEnabled(false); m_forwardAsAttachment->setEnabled(false); m_resend->setEnabled(false); } } void MainWindow::slotUpdateMessageActions() { Composer::RecipientList dummy; m_replyPrivate->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_PRIVATE, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyAllButMe->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL_BUT_ME, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyAll->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_ALL, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyList->setEnabled(Composer::Util::replyRecipientList(Composer::REPLY_LIST, senderIdentitiesModel(), m_messageWidget->messageView->currentMessage(), dummy)); m_replyGuess->setEnabled(m_replyPrivate->isEnabled() || m_replyAllButMe->isEnabled() || m_replyAll->isEnabled() || m_replyList->isEnabled()); // Check the default reply mode // I suspect this is not going to work for everybody. Suggestions welcome... if (m_replyList->isEnabled()) { m_replyButton->setDefaultAction(m_replyList); } else if (m_replyAllButMe->isEnabled()) { m_replyButton->setDefaultAction(m_replyAllButMe); } else { m_replyButton->setDefaultAction(m_replyPrivate); } m_forwardAsAttachment->setEnabled(m_messageWidget->messageView->currentMessage().isValid()); m_resend->setEnabled(m_messageWidget->messageView->currentMessage().isValid()); } void MainWindow::scrollMessageUp() { m_messageWidget->area->ensureVisible(0, 0, 0, 0); } void MainWindow::slotReplyTo() { m_messageWidget->messageView->reply(this, Composer::REPLY_PRIVATE); } void MainWindow::slotReplyAll() { m_messageWidget->messageView->reply(this, Composer::REPLY_ALL); } void MainWindow::slotReplyAllButMe() { m_messageWidget->messageView->reply(this, Composer::REPLY_ALL_BUT_ME); } void MainWindow::slotReplyList() { m_messageWidget->messageView->reply(this, Composer::REPLY_LIST); } void MainWindow::slotReplyGuess() { if (m_replyButton->defaultAction() == m_replyAllButMe) { slotReplyAllButMe(); } else if (m_replyButton->defaultAction() == m_replyAll) { slotReplyAll(); } else if (m_replyButton->defaultAction() == m_replyList) { slotReplyList(); } else { slotReplyTo(); } } void MainWindow::slotForwardAsAttachment() { m_messageWidget->messageView->forward(this, Composer::ForwardMode::FORWARD_AS_ATTACHMENT); } void MainWindow::slotResend() { QModelIndex index; Imap::Mailbox::Model::realTreeItem(msgListWidget->tree->currentIndex(), nullptr, &index); if (!index.isValid()) return; auto recipients = QList>(); for (const auto &kind: {Imap::Mailbox::RoleMessageTo, Imap::Mailbox::RoleMessageCc, Imap::Mailbox::RoleMessageBcc}) { for (const auto &oneAddr : index.data(kind).toList()) { Q_ASSERT(oneAddr.type() == QVariant::StringList); QStringList item = oneAddr.toStringList(); Q_ASSERT(item.size() == 4); Imap::Message::MailAddress a(item[0], item[1], item[2], item[3]); Composer::RecipientKind translatedKind = Composer::RecipientKind::ADDRESS_TO; switch (kind) { case Imap::Mailbox::RoleMessageTo: translatedKind = Composer::RecipientKind::ADDRESS_RESENT_TO; break; case Imap::Mailbox::RoleMessageCc: translatedKind = Composer::RecipientKind::ADDRESS_RESENT_CC; break; case Imap::Mailbox::RoleMessageBcc: translatedKind = Composer::RecipientKind::ADDRESS_RESENT_BCC; break; default: Q_ASSERT(false); break; } recipients.push_back({translatedKind, a.asPrettyString()}); } } ComposeWidget::warnIfMsaNotConfigured( ComposeWidget::createFromReadOnly(this, index, recipients), this); } void MainWindow::slotComposeMailUrl(const QUrl &url) { ComposeWidget::warnIfMsaNotConfigured(ComposeWidget::createFromUrl(this, url), this); } void MainWindow::slotManageContact(const QUrl &url) { Imap::Message::MailAddress addr; if (!Imap::Message::MailAddress::fromUrl(addr, url, QStringLiteral("x-trojita-manage-contact"))) return; Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook(); if (!addressbook) return; addressbook->openContactWindow(addr.mailbox + QLatin1Char('@') + addr.host, addr.name); } void MainWindow::invokeContactEditor() { Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook(); if (!addressbook) return; addressbook->openAddressbookWindow(); } /** @short Create an MSAFactory as per the settings */ MSA::MSAFactory *MainWindow::msaFactory() { using namespace Common; QString method = m_settings->value(SettingsNames::msaMethodKey).toString(); MSA::MSAFactory *msaFactory = 0; if (method == SettingsNames::methodSMTP || method == SettingsNames::methodSSMTP) { msaFactory = new MSA::SMTPFactory(m_settings->value(SettingsNames::smtpHostKey).toString(), m_settings->value(SettingsNames::smtpPortKey).toInt(), (method == SettingsNames::methodSSMTP), (method == SettingsNames::methodSMTP) && m_settings->value(SettingsNames::smtpStartTlsKey).toBool(), m_settings->value(SettingsNames::smtpAuthKey).toBool(), m_settings->value(SettingsNames::smtpAuthReuseImapCredsKey, false).toBool() ? m_settings->value(SettingsNames::imapUserKey).toString() : m_settings->value(SettingsNames::smtpUserKey).toString()); } else if (method == SettingsNames::methodSENDMAIL) { QStringList args = m_settings->value(SettingsNames::sendmailKey, SettingsNames::sendmailDefaultCmd).toString().split(QLatin1Char(' ')); if (args.isEmpty()) { return 0; } QString appName = args.takeFirst(); msaFactory = new MSA::SendmailFactory(appName, args); } else if (method == SettingsNames::methodImapSendmail) { if (!imapModel()->capabilities().contains(QStringLiteral("X-DRAFT-I01-SENDMAIL"))) { return 0; } msaFactory = new MSA::ImapSubmitFactory(qobject_cast(imapAccess()->imapModel())); } else { return 0; } return msaFactory; } void MainWindow::slotMailboxDeleteFailed(const QString &mailbox, const QString &msg) { Gui::Util::messageBoxWarning(this, tr("Can't delete mailbox"), tr("Deleting mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg)); } void MainWindow::slotMailboxCreateFailed(const QString &mailbox, const QString &msg) { Gui::Util::messageBoxWarning(this, tr("Can't create mailbox"), tr("Creating mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg)); } void MainWindow::slotMailboxSyncFailed(const QString &mailbox, const QString &msg) { Gui::Util::messageBoxWarning(this, tr("Can't open mailbox"), tr("Opening mailbox \"%1\" failed with the following message:\n%2").arg(mailbox, msg)); } void MainWindow::slotMailboxChanged(const QModelIndex &mailbox) { using namespace Imap::Mailbox; QString mailboxName = mailbox.data(RoleMailboxName).toString(); bool isSentMailbox = mailbox.isValid() && !mailboxName.isEmpty() && m_settings->value(Common::SettingsNames::composerSaveToImapKey).toBool() && mailboxName == m_settings->value(Common::SettingsNames::composerImapSentKey).toString(); QTreeView *tree = msgListWidget->tree; // Automatically trigger visibility of the TO and FROM columns if (isSentMailbox) { if (tree->isColumnHidden(MsgListModel::TO) && !tree->isColumnHidden(MsgListModel::FROM)) { tree->hideColumn(MsgListModel::FROM); tree->showColumn(MsgListModel::TO); } } else { if (tree->isColumnHidden(MsgListModel::FROM) && !tree->isColumnHidden(MsgListModel::TO)) { tree->hideColumn(MsgListModel::TO); tree->showColumn(MsgListModel::FROM); } } updateMessageFlags(); slotScrollToUnseenMessage(); } void MainWindow::showConnectionStatus(uint parserId, Imap::ConnectionState state) { Q_UNUSED(parserId); static Imap::ConnectionState previousState = Imap::ConnectionState::CONN_STATE_NONE; QString message = connectionStateToString(state); if (state == Imap::ConnectionState::CONN_STATE_SELECTED && previousState >= Imap::ConnectionState::CONN_STATE_SELECTED) { // prevent excessive popups when we "reset the state" to something which is shown quite often showStatusMessage(QString()); } else { showStatusMessage(message); } previousState = state; } void MainWindow::slotShowLinkTarget(const QString &link) { if (link.isEmpty()) { QToolTip::hideText(); } else { QToolTip::showText(QCursor::pos(), tr("Link target: %1").arg(UiUtils::Formatting::htmlEscaped(link))); } } void MainWindow::slotShowAboutTrojita() { Ui::AboutDialog ui; QDialog *widget = new QDialog(this); widget->setAttribute(Qt::WA_DeleteOnClose); ui.setupUi(widget); ui.versionLabel->setText(Common::Application::version); ui.qtVersion->setText(QStringLiteral("Qt " QT_VERSION_STR "")); connect(ui.qtVersion, &QLabel::linkActivated, qApp, &QApplication::aboutQt); std::vector> features; features.emplace_back(tr("Plugins"), #ifdef WITH_SHARED_PLUGINS true #else false #endif ); features.emplace_back(tr("Encrypted and signed messages"), #ifdef TROJITA_HAVE_CRYPTO_MESSAGES true #else false #endif ); features.emplace_back(tr("IMAP compression"), #ifdef TROJITA_HAVE_ZLIB true #else false #endif ); QString featuresText = QStringLiteral("
    "); for (const auto x: features) { featuresText += x.second ? tr("
  • %1: supported
  • ").arg(x.first) : tr("
  • %1: disabled
  • ").arg(x.first); } featuresText += QStringLiteral("
"); ui.descriptionLabel->setText(ui.descriptionLabel->text() + featuresText); QStringList copyright; { // Find the names of the authors and remove date codes from there QFile license(QStringLiteral(":/LICENSE")); license.open(QFile::ReadOnly); const QString prefix(QStringLiteral("Copyright (C) ")); Q_FOREACH(const QString &line, QString::fromUtf8(license.readAll()).split(QLatin1Char('\n'))) { if (line.startsWith(prefix)) { const int pos = prefix.size(); copyright << QChar(0xa9 /* COPYRIGHT SIGN */) + QLatin1Char(' ') + line.mid(pos).replace(QRegularExpression(QLatin1String("(\\d) - (\\d)")), QLatin1String("\\1") + QChar(0x2014 /* EM DASH */) + QLatin1String("\\2")); } } } ui.credits->setTextFormat(Qt::PlainText); ui.credits->setText(copyright.join(QStringLiteral("\n"))); widget->show(); } void MainWindow::slotDonateToTrojita() { QDesktopServices::openUrl(QStringLiteral("https://sourceforge.net/p/trojita/donate/")); } void MainWindow::slotSaveCurrentMessageBody() { Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; if (! item.data(Imap::Mailbox::RoleMessageUid).isValid()) continue; QModelIndex messageIndex = Imap::deproxifiedIndex(item); Imap::Network::MsgPartNetAccessManager *netAccess = new Imap::Network::MsgPartNetAccessManager(this); netAccess->setModelMessage(messageIndex); Imap::Network::FileDownloadManager *fileDownloadManager = new Imap::Network::FileDownloadManager(this, netAccess, messageIndex); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::succeeded, fileDownloadManager, &QObject::deleteLater); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError, fileDownloadManager, &QObject::deleteLater); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::fileNameRequested, this, &MainWindow::slotDownloadMessageFileNameRequested); connect(fileDownloadManager, &Imap::Network::FileDownloadManager::transferError, this, &MainWindow::slotDownloadTransferError); connect(fileDownloadManager, &QObject::destroyed, netAccess, &QObject::deleteLater); fileDownloadManager->downloadMessage(); } } void MainWindow::slotDownloadTransferError(const QString &errorString) { Gui::Util::messageBoxCritical(this, tr("Can't save into file"), tr("Unable to save into file. Error:\n%1").arg(errorString)); } void MainWindow::slotDownloadMessageFileNameRequested(QString *fileName) { *fileName = QFileDialog::getSaveFileName(this, tr("Save Message"), *fileName, QString(), 0, QFileDialog::HideNameFilterDetails); } void MainWindow::slotViewMsgSource() { Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; if (! item.data(Imap::Mailbox::RoleMessageUid).isValid()) continue; auto w = messageSourceWidget(item); //: Translators: %1 is the UID of a message (a number) and %2 is the name of a mailbox. w->setWindowTitle(tr("Message source of UID %1 in %2").arg( QString::number(item.data(Imap::Mailbox::RoleMessageUid).toUInt()), Imap::deproxifiedIndex(item).parent().parent().data(Imap::Mailbox::RoleMailboxName).toString() )); w->show(); } } QWidget *MainWindow::messageSourceWidget(const QModelIndex &message) { QModelIndex messageIndex = Imap::deproxifiedIndex(message); MessageSourceWidget *sourceWidget = new MessageSourceWidget(0, messageIndex); sourceWidget->setAttribute(Qt::WA_DeleteOnClose); QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), sourceWidget); sourceWidget->addAction(close); close->setShortcut(tr("Ctrl+W")); connect(close, &QAction::triggered, sourceWidget, &QWidget::close); return sourceWidget; } void MainWindow::slotViewMsgHeaders() { Q_FOREACH(const QModelIndex &item, msgListWidget->tree->selectionModel()->selectedIndexes()) { Q_ASSERT(item.isValid()); if (item.column() != 0) continue; if (! item.data(Imap::Mailbox::RoleMessageUid).isValid()) continue; QModelIndex messageIndex = Imap::deproxifiedIndex(item); auto widget = new MessageHeadersWidget(nullptr, messageIndex); widget->setAttribute(Qt::WA_DeleteOnClose); QAction *close = new QAction(UiUtils::loadIcon(QStringLiteral("window-close")), tr("Close"), widget); widget->addAction(close); close->setShortcut(tr("Ctrl+W")); connect(close, &QAction::triggered, widget, &QWidget::close); widget->show(); } } #ifdef XTUPLE_CONNECT void MainWindow::slotXtSyncCurrentMailbox() { QModelIndex index = mboxTree->currentIndex(); if (! index.isValid()) return; QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString(); QSettings s; QStringList mailboxes = s.value(Common::SettingsNames::xtSyncMailboxList).toStringList(); if (xtIncludeMailboxInSync->isChecked()) { if (! mailboxes.contains(mailbox)) { mailboxes.append(mailbox); } } else { mailboxes.removeAll(mailbox); } s.setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes); QSettings(QSettings::UserScope, QString::fromAscii("xTuple.com"), QString::fromAscii("xTuple")).setValue(Common::SettingsNames::xtSyncMailboxList, mailboxes); prettyMboxModel->xtConnectStatusChanged(index); } #endif void MainWindow::slotSubscribeCurrentMailbox() { QModelIndex index = mboxTree->currentIndex(); if (! index.isValid()) return; QString mailbox = index.data(Imap::Mailbox::RoleMailboxName).toString(); if (m_actionSubscribeMailbox->isChecked()) { imapModel()->subscribeMailbox(mailbox); } else { imapModel()->unsubscribeMailbox(mailbox); } } void MainWindow::slotShowOnlySubscribed() { if (m_actionShowOnlySubscribed->isEnabled()) { m_settings->setValue(Common::SettingsNames::guiMailboxListShowOnlySubscribed, m_actionShowOnlySubscribed->isChecked()); prettyMboxModel->setShowOnlySubscribed(m_actionShowOnlySubscribed->isChecked()); } } void MainWindow::slotScrollToUnseenMessage() { // Now this is much, much more reliable than messing around with finding out an "interesting message"... if (!m_actionSortNone->isChecked() && !m_actionSortThreading->isChecked()) { // we're using some funky sorting, better don't scroll anywhere } if (m_actionSortDescending->isChecked()) { msgListWidget->tree->scrollToTop(); } else { msgListWidget->tree->scrollToBottom(); } } void MainWindow::slotScrollToCurrent() { // TODO: begs for lambda if (QScrollBar *vs = msgListWidget->tree->verticalScrollBar()) { vs->setValue(vs->maximum() - vs->value()); // implies vs->minimum() == 0 } } void MainWindow::slotThreadMsgList() { // We want to save user's preferences and not override them with "threading disabled" when the server // doesn't report them, like in initial greetings. That's why we have to check for isEnabled() here. const bool useThreading = actionThreadMsgList->isChecked(); // Switching between threaded/unthreaded view shall reset the sorting criteria. The goal is to make // sorting rather seldomly used as people shall instead use proper threading. if (useThreading) { m_actionSortThreading->setEnabled(true); if (!m_actionSortThreading->isChecked()) m_actionSortThreading->trigger(); m_actionSortNone->setEnabled(false); } else { m_actionSortNone->setEnabled(true); if (!m_actionSortNone->isChecked()) m_actionSortNone->trigger(); m_actionSortThreading->setEnabled(false); } QPersistentModelIndex currentItem = msgListWidget->tree->currentIndex(); if (useThreading && actionThreadMsgList->isEnabled()) { msgListWidget->tree->setRootIsDecorated(true); qobject_cast(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(true); } else { msgListWidget->tree->setRootIsDecorated(false); qobject_cast(m_imapAccess->threadingMsgListModel())->setUserWantsThreading(false); } m_settings->setValue(Common::SettingsNames::guiMsgListShowThreading, QVariant(useThreading)); if (currentItem.isValid()) { msgListWidget->tree->scrollTo(currentItem); } else { // If we cannot determine the current item, at least scroll to a predictable place. Without this, the view // would jump to "weird" places, probably due to some heuristics about trying to show "roughly the same" // objects as what was visible before the reshuffling. slotScrollToUnseenMessage(); } } void MainWindow::slotSortingPreferenceChanged() { Qt::SortOrder order = m_actionSortDescending->isChecked() ? Qt::DescendingOrder : Qt::AscendingOrder; using namespace Imap::Mailbox; int column = -1; if (m_actionSortByArrival->isChecked()) { column = MsgListModel::RECEIVED_DATE; } else if (m_actionSortByCc->isChecked()) { column = MsgListModel::CC; } else if (m_actionSortByDate->isChecked()) { column = MsgListModel::DATE; } else if (m_actionSortByFrom->isChecked()) { column = MsgListModel::FROM; } else if (m_actionSortBySize->isChecked()) { column = MsgListModel::SIZE; } else if (m_actionSortBySubject->isChecked()) { column = MsgListModel::SUBJECT; } else if (m_actionSortByTo->isChecked()) { column = MsgListModel::TO; } else { column = -1; } msgListWidget->tree->header()->setSortIndicator(column, order); } void MainWindow::slotSortingConfirmed(int column, Qt::SortOrder order) { // don't do anything during initialization if (!m_actionSortNone) return; using namespace Imap::Mailbox; QAction *action; switch (column) { case MsgListModel::SEEN: case MsgListModel::FLAGGED: case MsgListModel::ATTACHMENT: case MsgListModel::COLUMN_COUNT: case MsgListModel::BCC: case -1: if (actionThreadMsgList->isChecked()) action = m_actionSortThreading; else action = m_actionSortNone; break; case MsgListModel::SUBJECT: action = m_actionSortBySubject; break; case MsgListModel::FROM: action = m_actionSortByFrom; break; case MsgListModel::TO: action = m_actionSortByTo; break; case MsgListModel::CC: action = m_actionSortByCc; break; case MsgListModel::DATE: action = m_actionSortByDate; break; case MsgListModel::RECEIVED_DATE: action = m_actionSortByArrival; break; case MsgListModel::SIZE: action = m_actionSortBySize; break; default: action = m_actionSortNone; } action->setChecked(true); if (order == Qt::DescendingOrder) m_actionSortDescending->setChecked(true); else m_actionSortAscending->setChecked(true); } void MainWindow::slotSearchRequested(const QStringList &searchConditions) { Imap::Mailbox::ThreadingMsgListModel * threadingMsgListModel = qobject_cast(m_imapAccess->threadingMsgListModel()); threadingMsgListModel->setUserSearchingSortingPreference(searchConditions, threadingMsgListModel->currentSortCriterium(), threadingMsgListModel->currentSortOrder()); } void MainWindow::slotHideRead() { const bool hideRead = actionHideRead->isChecked(); prettyMsgListModel->setHideRead(hideRead); m_settings->setValue(Common::SettingsNames::guiMsgListHideRead, QVariant(hideRead)); } void MainWindow::slotCapabilitiesUpdated(const QStringList &capabilities) { msgListWidget->tree->header()->viewport()->removeEventFilter(this); if (capabilities.contains(QStringLiteral("SORT"))) { m_actionSortByDate->actionGroup()->setEnabled(true); } else { m_actionSortByDate->actionGroup()->setEnabled(false); msgListWidget->tree->header()->viewport()->installEventFilter(this); } msgListWidget->setFuzzySearchSupported(capabilities.contains(QStringLiteral("SEARCH=FUZZY"))); m_actionShowOnlySubscribed->setEnabled(capabilities.contains(QStringLiteral("LIST-EXTENDED"))); m_actionShowOnlySubscribed->setChecked(m_actionShowOnlySubscribed->isEnabled() && m_settings->value( Common::SettingsNames::guiMailboxListShowOnlySubscribed, false).toBool()); m_actionSubscribeMailbox->setEnabled(m_actionShowOnlySubscribed->isEnabled()); const QStringList supportedCapabilities = Imap::Mailbox::ThreadingMsgListModel::supportedCapabilities(); Q_FOREACH(const QString &capability, capabilities) { if (supportedCapabilities.contains(capability)) { actionThreadMsgList->setEnabled(true); if (actionThreadMsgList->isChecked()) slotThreadMsgList(); return; } } actionThreadMsgList->setEnabled(false); } void MainWindow::slotShowImapInfo() { QString caps; Q_FOREACH(const QString &cap, imapModel()->capabilities()) { caps += tr("
  • %1
  • \n").arg(cap); } QString idString; if (!imapModel()->serverId().isEmpty() && imapModel()->capabilities().contains(QStringLiteral("ID"))) { QMap serverId = imapModel()->serverId(); #define IMAP_ID_FIELD(Var, Name) bool has_##Var = serverId.contains(Name); \ QString Var = has_##Var ? QString::fromUtf8(serverId[Name]).toHtmlEscaped() : tr("Unknown"); IMAP_ID_FIELD(serverName, "name"); IMAP_ID_FIELD(serverVersion, "version"); IMAP_ID_FIELD(os, "os"); IMAP_ID_FIELD(osVersion, "os-version"); IMAP_ID_FIELD(vendor, "vendor"); IMAP_ID_FIELD(supportUrl, "support-url"); IMAP_ID_FIELD(address, "address"); IMAP_ID_FIELD(date, "date"); IMAP_ID_FIELD(command, "command"); IMAP_ID_FIELD(arguments, "arguments"); IMAP_ID_FIELD(environment, "environment"); #undef IMAP_ID_FIELD if (has_serverName) { idString = tr("

    "); if (has_serverVersion) idString += tr("Server: %1 %2").arg(serverName, serverVersion); else idString += tr("Server: %1").arg(serverName); if (has_vendor) { idString += tr(" (%1)").arg(vendor); } if (has_os) { if (has_osVersion) idString += tr(" on %1 %2", "%1 is the operating system of an IMAP server and %2 is its version.").arg(os, osVersion); else idString += tr(" on %1", "%1 is the operationg system of an IMAP server.").arg(os); } idString += tr("

    "); } else { idString = tr("

    The IMAP server did not return usable information about itself.

    "); } QString fullId; for (QMap::const_iterator it = serverId.constBegin(); it != serverId.constEnd(); ++it) { fullId += tr("
  • %1: %2
  • ").arg(QString::fromUtf8(it.key()).toHtmlEscaped(), QString::fromUtf8(it.value()).toHtmlEscaped()); } idString += tr("
      %1
    ").arg(fullId); } else { idString = tr("

    The server has not provided information about its software version.

    "); } QMessageBox::information(this, tr("IMAP Server Information"), tr("%1" "

    The following capabilities are currently advertised:

    \n" "
      \n%2
    ").arg(idString, caps)); } QSize MainWindow::sizeHint() const { return QSize(1150, 980); } void MainWindow::slotUpdateWindowTitle() { QModelIndex mailbox = qobject_cast(m_imapAccess->msgListModel())->currentMailbox(); QString profileName = QString::fromUtf8(qgetenv("TROJITA_PROFILE")); if (!profileName.isEmpty()) profileName = QLatin1String(" [") + profileName + QLatin1Char(']'); if (mailbox.isValid()) { if (mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) { - setWindowTitle(trUtf8("%1 - %n unread - Trojitá", 0, mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) + setWindowTitle(tr("%1 - %n unread - Trojitá", 0, mailbox.data(Imap::Mailbox::RoleUnreadMessageCount).toInt()) .arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName); } else { - setWindowTitle(trUtf8("%1 - Trojitá").arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName); + setWindowTitle(tr("%1 - Trojitá").arg(mailbox.data(Imap::Mailbox::RoleShortMailboxName).toString()) + profileName); } } else { - setWindowTitle(trUtf8("Trojitá") + profileName); + setWindowTitle(tr("Trojitá") + profileName); } } void MainWindow::slotLayoutCompact() { saveSizesAndState(); if (!m_mainHSplitter) { m_mainHSplitter = new QSplitter(); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } if (!m_mainVSplitter) { m_mainVSplitter = new QSplitter(); m_mainVSplitter->setOrientation(Qt::Vertical); connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainVSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } m_mainVSplitter->addWidget(msgListWidget); m_mainVSplitter->addWidget(m_messageWidget); m_mainHSplitter->addWidget(mboxTree); m_mainHSplitter->addWidget(m_mainVSplitter); mboxTree->show(); msgListWidget->show(); m_messageWidget->show(); m_mainVSplitter->show(); m_mainHSplitter->show(); // The mboxTree shall not expand... m_mainHSplitter->setStretchFactor(0, 0); // ...while the msgListTree shall consume all the remaining space m_mainHSplitter->setStretchFactor(1, 1); // The CompleteMessageWidget shall not not collapse m_mainVSplitter->setCollapsible(m_mainVSplitter->indexOf(m_messageWidget), false); setCentralWidget(m_mainHSplitter); delete m_mainStack; m_layoutMode = LAYOUT_COMPACT; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutCompact); applySizesAndState(); } void MainWindow::slotLayoutWide() { saveSizesAndState(); if (!m_mainHSplitter) { m_mainHSplitter = new QSplitter(); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, m_delayedStateSaving, static_cast(&QTimer::start)); connect(m_mainHSplitter.data(), &QSplitter::splitterMoved, this, &MainWindow::possiblyLoadMessageOnSplittersChanged); } m_mainHSplitter->addWidget(mboxTree); m_mainHSplitter->addWidget(msgListWidget); m_mainHSplitter->addWidget(m_messageWidget); msgListWidget->resize(mboxTree->size()); m_messageWidget->resize(mboxTree->size()); m_mainHSplitter->setStretchFactor(0, 0); m_mainHSplitter->setStretchFactor(1, 1); m_mainHSplitter->setStretchFactor(2, 1); m_mainHSplitter->setCollapsible(m_mainHSplitter->indexOf(m_messageWidget), false); mboxTree->show(); msgListWidget->show(); m_messageWidget->show(); m_mainHSplitter->show(); setCentralWidget(m_mainHSplitter); delete m_mainStack; delete m_mainVSplitter; m_layoutMode = LAYOUT_WIDE; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutWide); applySizesAndState(); } void MainWindow::slotLayoutOneAtTime() { saveSizesAndState(); if (m_mainStack) return; m_mainStack = new OnePanelAtTimeWidget(this, mboxTree, msgListWidget, m_messageWidget, m_mainToolbar, m_oneAtTimeGoBack); setCentralWidget(m_mainStack); delete m_mainHSplitter; delete m_mainVSplitter; m_layoutMode = LAYOUT_ONE_AT_TIME; m_settings->setValue(Common::SettingsNames::guiMainWindowLayout, Common::SettingsNames::guiMainWindowLayoutOneAtTime); applySizesAndState(); } Imap::Mailbox::Model *MainWindow::imapModel() const { return qobject_cast(m_imapAccess->imapModel()); } void MainWindow::desktopGeometryChanged() { m_delayedStateSaving->start(); } QString MainWindow::settingsKeyForLayout(const LayoutMode layout) { switch (layout) { case LAYOUT_COMPACT: return Common::SettingsNames::guiSizesInMainWinWhenCompact; case LAYOUT_WIDE: return Common::SettingsNames::guiSizesInMainWinWhenWide; case LAYOUT_ONE_AT_TIME: return Common::SettingsNames::guiSizesInaMainWinWhenOneAtATime; break; } return QString(); } void MainWindow::saveSizesAndState() { if (m_skipSavingOfUI) return; QRect geometry = qApp->desktop()->availableGeometry(this); QString key = settingsKeyForLayout(m_layoutMode); if (key.isEmpty()) return; QList items; items << saveGeometry(); items << saveState(); items << (m_mainVSplitter ? m_mainVSplitter->saveState() : QByteArray()); items << (m_mainHSplitter ? m_mainHSplitter->saveState() : QByteArray()); items << msgListWidget->tree->header()->saveState(); items << QByteArray::number(msgListWidget->tree->header()->count()); for (int i = 0; i < msgListWidget->tree->header()->count(); ++i) { items << QByteArray::number(msgListWidget->tree->header()->sectionSize(i)); } // a bool cannot be pushed directly onto a QByteArray so we must convert it to a number items << QByteArray::number(menuBar()->isVisible()); QByteArray buf; QDataStream stream(&buf, QIODevice::WriteOnly); stream << items.size(); Q_FOREACH(const QByteArray &item, items) { stream << item; } m_settings->setValue(key.arg(QString::number(geometry.width())), buf); } void MainWindow::saveRawStateSetting(bool enabled) { m_settings->setValue(Common::SettingsNames::guiAllowRawSearch, enabled); } void MainWindow::applySizesAndState() { QRect geometry = qApp->desktop()->availableGeometry(this); QString key = settingsKeyForLayout(m_layoutMode); if (key.isEmpty()) return; QByteArray buf = m_settings->value(key.arg(QString::number(geometry.width()))).toByteArray(); if (buf.isEmpty()) return; int size; QDataStream stream(&buf, QIODevice::ReadOnly); stream >> size; QByteArray item; // There are slots connected to various events triggered by both restoreGeometry() and restoreState() which would attempt to // save our geometries and state, which is what we must avoid while this method is executing. bool skipSaving = m_skipSavingOfUI; m_skipSavingOfUI = true; if (size-- && !stream.atEnd()) { stream >> item; // https://bugreports.qt-project.org/browse/QTBUG-30636 if (windowState() & Qt::WindowMaximized) { // restoreGeometry(.) restores the wrong size for at least maximized window // However, QWidget does also not notice that the configure request for this // is ignored by many window managers (because users really don't like when windows // drop themselves out of maximization) and has a wrong QWidget::geometry() idea from // the wrong assumption the request would have been hononred. // So we just "fix" the internal geometry immediately afterwards to prevent // mislayouting // There's atm. no flicker due to this (and because Qt compresses events) // In case it ever occurs, we can frame this in setUpdatesEnabled(false/true) QRect oldGeometry = MainWindow::geometry(); restoreGeometry(item); if (windowState() & Qt::WindowMaximized) setGeometry(oldGeometry); } else { restoreGeometry(item); if (windowState() & Qt::WindowMaximized) { // ensure to try setting the proper geometry and have the WM constrain us setGeometry(QApplication::desktop()->availableGeometry()); } } } if (size-- && !stream.atEnd()) { stream >> item; restoreState(item); } if (size-- && !stream.atEnd()) { stream >> item; if (m_mainVSplitter) { m_mainVSplitter->restoreState(item); } } if (size-- && !stream.atEnd()) { stream >> item; if (m_mainHSplitter) { m_mainHSplitter->restoreState(item); } } if (size-- && !stream.atEnd()) { stream >> item; msgListWidget->tree->header()->restoreState(item); // got to manually update the state of the actions which control the visibility state msgListWidget->tree->updateActionsAfterRestoredState(); } connect(msgListWidget->tree->header(), &QHeaderView::sectionCountChanged, msgListWidget->tree, &MsgListView::slotHandleNewColumns); if (size-- && !stream.atEnd()) { stream >> item; bool ok; int columns = item.toInt(&ok); if (ok) { msgListWidget->tree->header()->setStretchLastSection(false); for (int i = 0; i < columns && size-- && !stream.atEnd(); ++i) { stream >> item; int sectionSize = item.toInt(); QHeaderView::ResizeMode resizeMode = msgListWidget->tree->resizeModeForColumn(i); if (sectionSize > 0 && resizeMode == QHeaderView::Interactive) { // fun fact: user cannot resize by mouse when size <= 0 msgListWidget->tree->setColumnWidth(i, sectionSize); } else { msgListWidget->tree->setColumnWidth(i, msgListWidget->tree->sizeHintForColumn(i)); } msgListWidget->tree->header()->setSectionResizeMode(i, resizeMode); } } } if (size-- && !stream.atEnd()) { stream >> item; bool ok; bool visibility = item.toInt(&ok); if (ok) { menuBar()->setVisible(visibility); showMenuBar->setChecked(visibility); } } m_skipSavingOfUI = skipSaving; } void MainWindow::resizeEvent(QResizeEvent *) { m_delayedStateSaving->start(); } /** @short Make sure that the message gets loaded after the splitters have changed their position */ void MainWindow::possiblyLoadMessageOnSplittersChanged() { if (m_messageWidget->isVisible() && !m_messageWidget->size().isEmpty()) { // We do not have to check whether it's a different message; the setMessage() will do this or us // and there are multiple proxy models involved anyway QModelIndex index = msgListWidget->tree->currentIndex(); if (index.isValid()) { // OTOH, setting an invalid QModelIndex would happily assert-fail m_messageWidget->messageView->setMessage(msgListWidget->tree->currentIndex()); } } } Imap::ImapAccess *MainWindow::imapAccess() const { return m_imapAccess; } void MainWindow::enableLoggingToDisk() { protocolLogger->slotSetPersistentLogging(true); } void MainWindow::slotPluginsChanged() { Plugins::AddressbookPlugin *addressbook = pluginManager()->addressbook(); if (!addressbook || !(addressbook->features() & Plugins::AddressbookPlugin::FeatureAddressbookWindow)) m_actionContactEditor->setEnabled(false); else m_actionContactEditor->setEnabled(true); } /** @short Update the default action to make sure that we show a correct status of the network connection */ void MainWindow::updateNetworkIndication() { if (QAction *action = qobject_cast(sender())) { if (action->isChecked()) { m_netToolbarDefaultAction->setIcon(action->icon()); } } } void MainWindow::showStatusMessage(const QString &message) { networkIndicator->setToolTip(message); if (isActiveWindow()) QToolTip::showText(networkIndicator->mapToGlobal(QPoint(0, 0)), message); } void MainWindow::slotMessageModelChanged(QAbstractItemModel *model) { mailMimeTree->setModel(model); } void MainWindow::slotFavoriteTagsChanged() { for (int i = 1; i <= m_favoriteTags->rowCount(); ++i) { QAction *action = ShortcutHandler::instance()->action(QStringLiteral("action_tag_") + QString::number(i)); if (action) action->setText(tr("Tag with \"%1\"").arg(m_favoriteTags->tagNameByIndex(i - 1))); } } void MainWindow::registerComposeWindow(ComposeWidget* widget) { connect(widget, &ComposeWidget::logged, this, [this](const Common::LogKind kind, const QString& source, const QString& message) { protocolLogger->log(0, Common::LogMessage(QDateTime::currentDateTime(), kind, source, message, 0)); }); } } diff --git a/src/Gui/main.cpp b/src/Gui/main.cpp index 92a8a0d0..56154ed7 100644 --- a/src/Gui/main.cpp +++ b/src/Gui/main.cpp @@ -1,219 +1,219 @@ /* Copyright (C) 2006 - 2015 Jan Kundrát 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 "AppVersion/SetCoreApplication.h" #include "Common/Application.h" #include "Common/MetaTypes.h" #include "Common/SettingsNames.h" #include "Gui/Util.h" #include "Gui/Window.h" #include "IPC/IPC.h" #include "UiUtils/IconLoader.h" #include "static_plugins.h" int main(int argc, char **argv) { Common::registerMetaTypes(); QApplication app(argc, argv); Q_INIT_RESOURCE(icons); Q_INIT_RESOURCE(license); QTranslator qtTranslator; qtTranslator.load(QLatin1String("qt_") + QLocale::system().name(), QLibraryInfo::location(QLibraryInfo::TranslationsPath)); app.installTranslator(&qtTranslator); QLatin1String localeSuffix("/locale"); QString localeName(QLatin1String("trojita_common_") + (qgetenv("KDE_LANG") == "x-test" ? QStringLiteral("x_test") : QLocale::system().name())); // The "installed to system" localization QTranslator appSystemTranslator; if (!Gui::Util::pkgDataDir().isEmpty()) { appSystemTranslator.load(localeName, Gui::Util::pkgDataDir() + localeSuffix); app.installTranslator(&appSystemTranslator); } // The "in the directory with the binary" localization QTranslator appDirectoryTranslator; appDirectoryTranslator.load(localeName, app.applicationDirPath() + localeSuffix); app.installTranslator(&appDirectoryTranslator); AppVersion::setGitVersion(); AppVersion::setCoreApplicationData(); app.setAttribute(Qt::AA_UseHighDpiPixmaps); #if QT_VERSION >= QT_VERSION_CHECK(5, 7, 0) app.setDesktopFileName(QStringLiteral("org.kde.trojita")); #endif app.setWindowIcon(UiUtils::loadIcon(QStringLiteral("trojita"))); QTextStream qOut(stdout, QIODevice::WriteOnly); QTextStream qErr(stderr, QIODevice::WriteOnly); const QStringList &arguments = app.arguments(); bool error = false; bool showHelp = false; bool showMainWindow = false; bool showComposeWindow = false; bool showAddressbookWindow = false; bool logToDisk = false; QString profileName; QString url; for (int i = 1; i < arguments.size(); ++i) { const QString &arg = arguments.at(i); if (arg.startsWith(QLatin1Char('-'))) { if (arg == QLatin1String("-m") || arg == QLatin1String("--mainwindow")) { showMainWindow = true; } else if (arg == QLatin1String("-a") || arg == QLatin1String("--addressbook")) { showAddressbookWindow = true; } else if (arg == QLatin1String("-c") || arg == QLatin1String("--compose")) { showComposeWindow = true; } else if (arg == QLatin1String("-h") || arg == QLatin1String("--help")) { showHelp = true; } else if (arg == QLatin1String("-p") || arg == QLatin1String("--profile")) { if (i+1 == arguments.size() || arguments.at(i+1).startsWith(QLatin1Char('-'))) { qErr << QObject::tr("Error: Profile was not specified") << endl; error = true; break; } else if (!profileName.isEmpty()) { qErr << QObject::tr("Error: Duplicate profile option '%1'").arg(arg) << endl; error = true; break; } else { profileName = arguments.at(i+1); ++i; } } else if (arg == QLatin1String("--log-to-disk")) { logToDisk = true; } else { qErr << QObject::tr("Warning: Unknown option '%1'").arg(arg) << endl; } } else { if (!url.isEmpty() || !arg.startsWith(QLatin1String("mailto:"))) { qErr << QObject::tr("Warning: Unexpected argument '%1'").arg(arg) << endl; } else { url = arg; showComposeWindow = true; } } } if (!showMainWindow && !showComposeWindow && !showAddressbookWindow) showMainWindow = true; if (error) showHelp = true; if (showHelp) { - qOut << endl << QObject::trUtf8( + qOut << endl << QObject::tr( "Usage: %1 [options] [url]\n" "\n" "Trojitá %2 - fast Qt IMAP e-mail client\n" "\n" "Options:\n" " -h, --help Show this help\n" " -m, --mainwindow Show main window (default when no option is provided)\n" " -a, --addressbook Show addressbook window\n" " -c, --compose Compose new email (default when url is provided)\n" " -p, --profile Set profile (cannot start with char '-')\n" " --log-to-disk Activate debug traffic logging to disk by default\n" "\n" "Arguments:\n" " url Mailto: url address for composing new email\n" ).arg(arguments.at(0), Common::Application::version) << endl; return error ? 1 : 0; } // Hack: support multiple "profiles" if (!profileName.isEmpty()) { // We are abusing the env vars here. Yes, it's a hidden global. Yes, it's ugly. // Take it or leave it, this is a time-limited hack. // The env var is also in UTF-8. I like UTF-8. qputenv("TROJITA_PROFILE", profileName.toUtf8()); } else { #ifndef Q_OS_WIN32 unsetenv("TROJITA_PROFILE"); #else putenv("TROJITA_PROFILE="); #endif } if (IPC::Instance::isRunning()) { if (showMainWindow) IPC::Instance::showMainWindow(); if (showAddressbookWindow) IPC::Instance::showAddressbookWindow(); if (showComposeWindow) IPC::Instance::composeMail(url); return 0; } QSettings settings(Common::Application::organization, profileName.isEmpty() ? Common::Application::name : Common::Application::name + QLatin1Char('-') + profileName); Gui::MainWindow win(&settings); QString errmsg; if (!IPC::registerInstance(&win, errmsg)) qErr << QObject::tr("Error: Registering IPC instance failed: %1").arg(errmsg) << endl; if ( settings.value(Common::SettingsNames::guiStartMinimized, QVariant(false)).toBool() ) { if ( !settings.value(Common::SettingsNames::guiShowSystray, QVariant(true)).toBool() ) { win.show(); win.setWindowState(Qt::WindowMinimized); } } else { win.show(); } if (showAddressbookWindow) win.invokeContactEditor(); if (showComposeWindow) { if (url.isEmpty()) win.slotComposeMail(); else win.slotComposeMailUrl(QUrl::fromEncoded(url.toUtf8())); } if (logToDisk) { win.enableLoggingToDisk(); } return app.exec(); } diff --git a/src/Imap/Model/Model.cpp b/src/Imap/Model/Model.cpp index 5f0614ec..3a55f479 100644 --- a/src/Imap/Model/Model.cpp +++ b/src/Imap/Model/Model.cpp @@ -1,2050 +1,2050 @@ /* Copyright (C) 2006 - 2014 Jan Kundrát 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 "Model.h" #include "Common/FindWithUnknown.h" #include "Common/InvokeMethod.h" #include "Imap/Encoders.h" #include "Imap/Model/ItemRoles.h" #include "Imap/Model/MailboxTree.h" #include "Imap/Model/SpecialFlagNames.h" #include "Imap/Model/TaskPresentationModel.h" #include "Imap/Model/Utils.h" #include "Imap/Tasks/AppendTask.h" #include "Imap/Tasks/CreateMailboxTask.h" #include "Imap/Tasks/GetAnyConnectionTask.h" #include "Imap/Tasks/KeepMailboxOpenTask.h" #include "Imap/Tasks/OpenConnectionTask.h" #include "Imap/Tasks/UpdateFlagsTask.h" #include "Imap/Tasks/CopyMoveMessagesTask.h" #include "Streams/SocketFactory.h" //#define DEBUG_PERIODICALLY_DUMP_TASKS //#define DEBUG_TASK_ROUTING namespace { using namespace Imap::Mailbox; /** @short Return true iff the two mailboxes have the same name It's an error to call this function on anything else but a mailbox. */ bool MailboxNamesEqual(const TreeItem *const a, const TreeItem *const b) { const TreeItemMailbox *const mailboxA = dynamic_cast(a); const TreeItemMailbox *const mailboxB = dynamic_cast(b); Q_ASSERT(mailboxA); Q_ASSERT(mailboxB); return mailboxA->mailbox() == mailboxB->mailbox(); } /** @short Mailbox name comparator to be used when sorting mailbox names The special-case mailbox name, the "INBOX", is always sorted as the first one. */ bool MailboxNameComparator(const TreeItem *const a, const TreeItem *const b) { const TreeItemMailbox *const mailboxA = dynamic_cast(a); const TreeItemMailbox *const mailboxB = dynamic_cast(b); if (mailboxA->mailbox() == QLatin1String("INBOX")) return true; if (mailboxB->mailbox() == QLatin1String("INBOX")) return false; return mailboxA->mailbox().compare(mailboxB->mailbox(), Qt::CaseInsensitive) < 1; } bool uidComparator(const TreeItem *const item, const uint uid) { const TreeItemMessage *const message = static_cast(item); uint messageUid = message->uid(); Q_ASSERT(messageUid); return messageUid < uid; } bool messageHasUidZero(const TreeItem *const item) { const TreeItemMessage *const message = static_cast(item); return message->uid() == 0; } } namespace Imap { namespace Mailbox { Model::Model(QObject *parent, std::shared_ptr cache, SocketFactoryPtr socketFactory, TaskFactoryPtr taskFactory) : QAbstractItemModel(parent) , m_cache(cache) , m_socketFactory(std::move(socketFactory)) , m_taskFactory(std::move(taskFactory)) , m_maxParsers(4) , m_mailboxes(nullptr) , m_netPolicy(NETWORK_OFFLINE) , m_taskModel(nullptr) , m_hasImapPassword(PasswordAvailability::NOT_REQUESTED) { m_startTls = m_socketFactory->startTlsRequired(); m_mailboxes = new TreeItemMailbox(0); onlineMessageFetch << QStringLiteral("ENVELOPE") << QStringLiteral("BODYSTRUCTURE") << QStringLiteral("RFC822.SIZE") << QStringLiteral("UID") << QStringLiteral("FLAGS"); EMIT_LATER_NOARG(this, networkPolicyOffline); EMIT_LATER_NOARG(this, networkPolicyChanged); #ifdef DEBUG_PERIODICALLY_DUMP_TASKS QTimer *periodicTaskDumper = new QTimer(this); periodicTaskDumper->setInterval(1000); connect(periodicTaskDumper, &QTimer::timeout, this, &Model::slotTasksChanged); periodicTaskDumper->start(); #endif m_taskModel = new TaskPresentationModel(this); m_periodicMailboxNumbersRefresh = new QTimer(this); // polling every five minutes m_periodicMailboxNumbersRefresh->setInterval(5 * 60 * 1000); connect(m_periodicMailboxNumbersRefresh, &QTimer::timeout, this, &Model::invalidateAllMessageCounts); } Model::~Model() { delete m_mailboxes; } /** @short Process responses from all sockets */ void Model::responseReceived() { for (QMap::iterator it = m_parsers.begin(); it != m_parsers.end(); ++it) { responseReceived(it); } } /** @short Process responses from the specified parser */ void Model::responseReceived(Parser *parser) { QMap::iterator it = m_parsers.find(parser); if (it == m_parsers.end()) { // This is a queued signal, so it's perfectly possible that the sender is gone already return; } responseReceived(it); } /** @short Process responses from the specified parser */ void Model::responseReceived(const QMap::iterator it) { Q_ASSERT(it->parser); int counter = 0; while (it->parser && it->parser->hasResponse()) { QSharedPointer resp = it->parser->getResponse(); Q_ASSERT(resp); // Always log BAD responses from a central place. They're bad enough to warant an extra treatment. // FIXME: is it worth an UI popup? if (Responses::State *stateResponse = dynamic_cast(resp.data())) { if (stateResponse->kind == Responses::BAD) { QString buf; QTextStream s(&buf); s << *stateResponse; logTrace(it->parser->parserId(), Common::LOG_OTHER, QStringLiteral("Model"), QStringLiteral("BAD response: %1").arg(buf)); qDebug() << buf; } } try { /* At this point, we want to iterate over all active tasks and try them for processing the server's responses (the plug() method). However, this is rather complex -- this call to plug() could result in signals being emitted, and certain slots connected to those signals might in turn want to queue more Tasks. Therefore, it->activeTasks could be modified, some items could be appended to it using the QList::append, which in turn could cause a realloc to happen, happily invalidating our iterators, and that kind of sucks. So, we have to iterate over a copy of the original list and instead of deleting Tasks, we store them into a temporary list. When we're done with processing, we walk the original list once again and simply remove all "deleted" items for real. This took me 3+ hours to track it down to what the hell was happening here, even though the underlying reason is simple -- QList::append() could invalidate existing iterators. */ bool handled = false; QList taskSnapshot = it->activeTasks; QList deletedTasks; QList::const_iterator taskEnd = taskSnapshot.constEnd(); // Try various tasks, perhaps it's their response. Also check if they're already finished and remove them. for (QList::const_iterator taskIt = taskSnapshot.constBegin(); taskIt != taskEnd; ++taskIt) { if (! handled) { #ifdef DEBUG_TASK_ROUTING try { logTrace(it->parser->parserId(), Common::LOG_TASKS, QString(), QString::fromAscii("Routing to %1 %2").arg(QString::fromAscii((*taskIt)->metaObject()->className()), (*taskIt)->debugIdentification())); #endif handled = resp->plug(*taskIt); #ifdef DEBUG_TASK_ROUTING if (handled) { logTrace(it->parser->parserId(), Common::LOG_TASKS, (*taskIt)->debugIdentification(), QLatin1String("Handled")); } } catch (std::exception &e) { logTrace(it->parser->parserId(), Common::LOG_TASKS, (*taskIt)->debugIdentification(), QLatin1String("Got exception when handling")); throw; } #endif } if ((*taskIt)->isFinished()) { deletedTasks << *taskIt; } } removeDeletedTasks(deletedTasks, it->activeTasks); runReadyTasks(); if (! handled) { resp->plug(it->parser, this); #ifdef DEBUG_TASK_ROUTING if (it->parser) { logTrace(it->parser->parserId(), Common::LOG_TASKS, QLatin1String("Model"), QLatin1String("Handled")); } else { logTrace(0, Common::LOG_TASKS, QLatin1String("Model"), QLatin1String("Handled")); } #endif } } catch (Imap::ImapException &e) { uint parserId = it->parser->parserId(); killParser(it->parser, PARSER_KILL_HARD); broadcastParseError(parserId, QString::fromStdString(e.exceptionClass()), QString::fromUtf8(e.what()), e.line(), e.offset()); break; } // Return to the event loop every 100 messages to handle GUI events ++counter; if (counter == 100) { QTimer::singleShot(0, this, SLOT(responseReceived())); break; } } if (!it->parser) { // He's dead, Jim m_taskModel->beginResetModel(); killParser(it.key(), PARSER_JUST_DELETE_LATER); m_parsers.erase(it); m_taskModel->endResetModel(); } } void Model::handleState(Imap::Parser *ptr, const Imap::Responses::State *const resp) { // OK/NO/BAD/PREAUTH/BYE using namespace Imap::Responses; const QByteArray &tag = resp->tag; if (!tag.isEmpty()) { if (tag == accessParser(ptr).logoutCmd) { // The LOGOUT is special, as it isn't associated with any task killParser(ptr, PARSER_KILL_EXPECTED); } else { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; // Unhandled command -- this is *extremely* weird throw CantHappen("The following command should have been handled elsewhere", *resp); } } else { // untagged response // FIXME: we should probably just eat them and don't bother, as untagged OK/NO could be rather common... switch (resp->kind) { case BYE: if (accessParser(ptr).logoutCmd.isEmpty()) { // The connection got closed but we haven't really requested that -- we better treat that as error, including // going offline... // ... but before that, expect that the connection will get closed soon changeConnectionState(ptr, CONN_STATE_LOGOUT); EMIT_LATER(this, imapError, Q_ARG(QString, resp->message)); setNetworkPolicy(NETWORK_OFFLINE); } if (accessParser(ptr).parser) { // previous block could enter the event loop and hence kill our parser; we shouldn't try to kill it twice killParser(ptr, PARSER_KILL_EXPECTED); } break; case OK: if (resp->respCode == NONE) { // This one probably should not be logged at all; dovecot sends these reponses to keep NATted connections alive break; } else { logTrace(ptr->parserId(), Common::LOG_OTHER, QString(), QStringLiteral("Warning: unhandled untagged OK with a response code")); break; } case NO: logTrace(ptr->parserId(), Common::LOG_OTHER, QString(), QStringLiteral("Warning: unhandled untagged NO...")); break; default: throw UnexpectedResponseReceived("Unhandled untagged response, sorry", *resp); } } } void Model::finalizeList(Parser *parser, TreeItemMailbox *mailboxPtr) { TreeItemChildrenList mailboxes; QList &listResponses = accessParser(parser).listResponses; const QString prefix = mailboxPtr->mailbox() + mailboxPtr->separator(); for (QList::iterator it = listResponses.begin(); it != listResponses.end(); /* nothing */) { if (it->mailbox == mailboxPtr->mailbox() || it->mailbox == prefix) { // rubbish, ignore it = listResponses.erase(it); } else if (it->mailbox.startsWith(prefix)) { if (!mailboxPtr->separator().isEmpty() && it->mailbox.midRef(prefix.length()).contains(mailboxPtr->separator())) { // This is about a mailbox which is nested deeper beneath the current thing (e.g., we're listing A.B.%, // and the current response is A.B.C.1), so let's assume that it's some else's LIST response. // The separator/delimiter checking is (hopefully) correct -- see https://tools.ietf.org/html/rfc3501#page-70 . ++it; } else { mailboxes << new TreeItemMailbox(mailboxPtr, *it); it = listResponses.erase(it); } } else { // it clearly is someone else's LIST response ++it; } } qSort(mailboxes.begin(), mailboxes.end(), MailboxNameComparator); // Remove duplicates; would be great if this could be done in a STLish way, // but unfortunately std::unique won't help here (the "duped" part of the // sequence contains undefined items) if (mailboxes.size() > 1) { auto it = mailboxes.begin(); // We've got to ignore the first one, that's the message list ++it; while (it != mailboxes.end()) { if (MailboxNamesEqual(it[-1], *it)) { delete *it; it = mailboxes.erase(it); } else { ++it; } } } QList metadataToCache; TreeItemChildrenList mailboxesWithoutChildren; for (auto it = mailboxes.constBegin(); it != mailboxes.constEnd(); ++it) { TreeItemMailbox *mailbox = dynamic_cast(*it); Q_ASSERT(mailbox); metadataToCache.append(mailbox->mailboxMetadata()); if (mailbox->hasNoChildMailboxesAlreadyKnown()) { mailboxesWithoutChildren << mailbox; } } cache()->setChildMailboxes(mailboxPtr->mailbox(), metadataToCache); for (auto it = mailboxesWithoutChildren.constBegin(); it != mailboxesWithoutChildren.constEnd(); ++it) cache()->setChildMailboxes(static_cast(*it)->mailbox(), QList()); replaceChildMailboxes(mailboxPtr, mailboxes); } void Model::finalizeIncrementalList(Parser *parser, const QString &parentMailboxName) { TreeItemMailbox *parentMbox = findParentMailboxByName(parentMailboxName); if (! parentMbox) { qDebug() << "Weird, no idea where to put the newly created mailbox" << parentMailboxName; return; } QList mailboxes; QList &listResponses = accessParser(parser).listResponses; for (QList::iterator it = listResponses.begin(); it != listResponses.end(); /* nothing */) { if (it->mailbox == parentMailboxName) { mailboxes << new TreeItemMailbox(parentMbox, *it); it = listResponses.erase(it); } else { // it clearly is someone else's LIST response ++it; } } qSort(mailboxes.begin(), mailboxes.end(), MailboxNameComparator); if (mailboxes.size() == 0) { qDebug() << "Weird, no matching LIST response for our prompt after CREATE"; qDeleteAll(mailboxes); return; } else if (mailboxes.size() > 1) { qDebug() << "Weird, too many LIST responses for our prompt after CREATE"; qDeleteAll(mailboxes); return; } auto it = parentMbox->m_children.begin(); Q_ASSERT(it != parentMbox->m_children.end()); ++it; while (it != parentMbox->m_children.end() && MailboxNameComparator(*it, mailboxes[0])) ++it; QModelIndex parentIdx = parentMbox == m_mailboxes ? QModelIndex() : parentMbox->toIndex(this); if (it == parentMbox->m_children.end()) beginInsertRows(parentIdx, parentMbox->m_children.size(), parentMbox->m_children.size()); else beginInsertRows(parentIdx, (*it)->row(), (*it)->row()); parentMbox->m_children.insert(it, mailboxes[0]); endInsertRows(); } void Model::replaceChildMailboxes(TreeItemMailbox *mailboxPtr, const TreeItemChildrenList &mailboxes) { /* Previously, we would call layoutAboutToBeChanged() and layoutChanged() here, but it resulted in invalid memory access in the attached QSortFilterProxyModels like this one: ==23294== Invalid read of size 4 ==23294== at 0x5EA34B1: QSortFilterProxyModelPrivate::index_to_iterator(QModelIndex const&) const (qsortfilterproxymodel.cpp:191) ==23294== by 0x5E9F8A3: QSortFilterProxyModel::parent(QModelIndex const&) const (qsortfilterproxymodel.cpp:1654) ==23294== by 0x5C5D45D: QModelIndex::parent() const (qabstractitemmodel.h:389) ==23294== by 0x5E47C48: QTreeView::drawRow(QPainter*, QStyleOptionViewItem const&, QModelIndex const&) const (qtreeview.cpp:1479) ==23294== by 0x5E479D9: QTreeView::drawTree(QPainter*, QRegion const&) const (qtreeview.cpp:1441) ==23294== by 0x5E4703A: QTreeView::paintEvent(QPaintEvent*) (qtreeview.cpp:1274) ==23294== by 0x5810C30: QWidget::event(QEvent*) (qwidget.cpp:8346) ==23294== by 0x5C91D03: QFrame::event(QEvent*) (qframe.cpp:557) ==23294== by 0x5D4259C: QAbstractScrollArea::viewportEvent(QEvent*) (qabstractscrollarea.cpp:1043) ==23294== by 0x5DFFD6E: QAbstractItemView::viewportEvent(QEvent*) (qabstractitemview.cpp:1619) ==23294== by 0x5E46EE0: QTreeView::viewportEvent(QEvent*) (qtreeview.cpp:1256) ==23294== by 0x5D43110: QAbstractScrollAreaPrivate::viewportEvent(QEvent*) (qabstractscrollarea_p.h:100) ==23294== Address 0x908dbec is 20 bytes inside a block of size 24 free'd ==23294== at 0x4024D74: operator delete(void*) (vg_replace_malloc.c:346) ==23294== by 0x5EA5236: void qDeleteAll::const_iterator>(QHash::const_iterator, QHash::const_iterator) (qalgorithms.h:322) ==23294== by 0x5EA3C06: void qDeleteAll >(QHash const&) (qalgorithms.h:330) ==23294== by 0x5E9E64B: QSortFilterProxyModelPrivate::_q_sourceLayoutChanged() (qsortfilterproxymodel.cpp:1249) ==23294== by 0x5EA29EC: QSortFilterProxyModel::qt_metacall(QMetaObject::Call, int, void**) (moc_qsortfilterproxymodel.cpp:133) ==23294== by 0x80EB205: Imap::Mailbox::PrettyMailboxModel::qt_metacall(QMetaObject::Call, int, void**) (moc_PrettyMailboxModel.cpp:64) ==23294== by 0x65D3EAD: QMetaObject::metacall(QObject*, QMetaObject::Call, int, void**) (qmetaobject.cpp:237) ==23294== by 0x65E8D7C: QMetaObject::activate(QObject*, QMetaObject const*, int, void**) (qobject.cpp:3272) ==23294== by 0x664A7E8: QAbstractItemModel::layoutChanged() (moc_qabstractitemmodel.cpp:161) ==23294== by 0x664A354: QAbstractItemModel::qt_metacall(QMetaObject::Call, int, void**) (moc_qabstractitemmodel.cpp:118) ==23294== by 0x5E9A3A9: QAbstractProxyModel::qt_metacall(QMetaObject::Call, int, void**) (moc_qabstractproxymodel.cpp:67) ==23294== by 0x80EAF3D: Imap::Mailbox::MailboxModel::qt_metacall(QMetaObject::Call, int, void**) (moc_MailboxModel.cpp:81) I have no idea why something like that happens -- layoutChanged() should be a hint that the indexes are gone now, which means that the code should *not* use tham after that point. That's just weird. */ QModelIndex parent = mailboxPtr == m_mailboxes ? QModelIndex() : mailboxPtr->toIndex(this); if (mailboxPtr->m_children.size() != 1) { // There's something besides the TreeItemMsgList and we're going to // overwrite them, so we have to delete them right now int count = mailboxPtr->rowCount(this); beginRemoveRows(parent, 1, count - 1); auto oldItems = mailboxPtr->setChildren(TreeItemChildrenList()); endRemoveRows(); qDeleteAll(oldItems); } if (! mailboxes.isEmpty()) { beginInsertRows(parent, 1, mailboxes.size()); auto dummy = mailboxPtr->setChildren(mailboxes); endInsertRows(); Q_ASSERT(dummy.isEmpty()); } else { auto dummy = mailboxPtr->setChildren(mailboxes); Q_ASSERT(dummy.isEmpty()); } emit dataChanged(parent, parent); } void Model::emitMessageCountChanged(TreeItemMailbox *const mailbox) { TreeItemMsgList *list = static_cast(mailbox->m_children[0]); QModelIndex msgListIndex = list->toIndex(this); emit dataChanged(msgListIndex, msgListIndex); QModelIndex mailboxIndex = mailbox->toIndex(this); emit dataChanged(mailboxIndex, mailboxIndex); emit messageCountPossiblyChanged(mailboxIndex); } void Model::handleCapability(Imap::Parser *ptr, const Imap::Responses::Capability *const resp) { updateCapabilities(ptr, resp->capabilities); } void Model::handleNumberResponse(Imap::Parser *ptr, const Imap::Responses::NumberResponse *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; throw UnexpectedResponseReceived("[Tasks API Port] Unhandled NumberResponse", *resp); } void Model::handleList(Imap::Parser *ptr, const Imap::Responses::List *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; accessParser(ptr).listResponses << *resp; } void Model::handleFlags(Imap::Parser *ptr, const Imap::Responses::Flags *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; throw UnexpectedResponseReceived("[Tasks API Port] Unhandled Flags", *resp); } void Model::handleSearch(Imap::Parser *ptr, const Imap::Responses::Search *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; throw UnexpectedResponseReceived("[Tasks API Port] Unhandled Search", *resp); } void Model::handleESearch(Imap::Parser *ptr, const Imap::Responses::ESearch *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; throw UnexpectedResponseReceived("Unhandled ESEARCH", *resp); } void Model::handleStatus(Imap::Parser *ptr, const Imap::Responses::Status *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; Q_UNUSED(ptr); TreeItemMailbox *mailbox = findMailboxByName(resp->mailbox); if (! mailbox) { qDebug() << "Couldn't find out which mailbox is" << resp->mailbox << "when parsing a STATUS reply"; return; } TreeItemMsgList *list = dynamic_cast(mailbox->m_children[0]); Q_ASSERT(list); bool updateCache = false; Imap::Responses::Status::stateDataType::const_iterator it = resp->states.constEnd(); if ((it = resp->states.constFind(Imap::Responses::Status::MESSAGES)) != resp->states.constEnd()) { updateCache |= list->m_totalMessageCount != static_cast(it.value()); list->m_totalMessageCount = it.value(); } if ((it = resp->states.constFind(Imap::Responses::Status::UNSEEN)) != resp->states.constEnd()) { updateCache |= list->m_unreadMessageCount != static_cast(it.value()); list->m_unreadMessageCount = it.value(); } if ((it = resp->states.constFind(Imap::Responses::Status::RECENT)) != resp->states.constEnd()) { updateCache |= list->m_recentMessageCount != static_cast(it.value()); list->m_recentMessageCount = it.value(); } list->m_numberFetchingStatus = TreeItem::DONE; emitMessageCountChanged(mailbox); if (updateCache) { // We have to be very careful to only touch the bits which are *not* used by the mailbox syncing code. // This is absolutely crucial -- STATUS is just a meaningless indicator, and stuff like the UID mapping // is definitely *not* updated at the same time. That's also why we completely ignore any data whatsoever // from the TreeItemMailbox' syncState, if any, and just work with the cache directly. auto state = cache()->mailboxSyncState(mailbox->mailbox()); // We are *not* updating the total message count as that conflicts with the mailbox syncing if (list->m_unreadMessageCount != -1) { state.setUnSeenCount(list->m_unreadMessageCount); } if (list->m_recentMessageCount != -1) { state.setRecent(list->m_recentMessageCount); } cache()->setMailboxSyncState(mailbox->mailbox(), state); } } void Model::handleFetch(Imap::Parser *ptr, const Imap::Responses::Fetch *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; throw UnexpectedResponseReceived("[Tasks API Port] Unhandled Fetch", *resp); } void Model::handleNamespace(Imap::Parser *ptr, const Imap::Responses::Namespace *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; return; // because it's broken and won't fly Q_UNUSED(ptr); Q_UNUSED(resp); } void Model::handleSort(Imap::Parser *ptr, const Imap::Responses::Sort *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; throw UnexpectedResponseReceived("[Tasks API Port] Unhandled Sort", *resp); } void Model::handleThread(Imap::Parser *ptr, const Imap::Responses::Thread *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; throw UnexpectedResponseReceived("[Tasks API Port] Unhandled Thread", *resp); } void Model::handleId(Parser *ptr, const Responses::Id *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; throw UnexpectedResponseReceived("Unhandled ID response", *resp); } void Model::handleEnabled(Parser *ptr, const Responses::Enabled *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; throw UnexpectedResponseReceived("Unhandled ENABLED response", *resp); } void Model::handleVanished(Parser *ptr, const Responses::Vanished *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; throw UnexpectedResponseReceived("Unhandled VANISHED response", *resp); } void Model::handleGenUrlAuth(Parser *ptr, const Responses::GenUrlAuth *const resp) { if (accessParser(ptr).connState == CONN_STATE_LOGOUT) return; throw UnexpectedResponseReceived("Unhandled GENURLAUTH response", *resp); } void Model::handleSocketEncryptedResponse(Parser *ptr, const Responses::SocketEncryptedResponse *const resp) { Q_UNUSED(resp); logTrace(ptr->parserId(), Common::LOG_IO_READ, QStringLiteral("Model"), QStringLiteral("Information about the SSL state not handled by the upper layers -- disconnect?")); killParser(ptr, PARSER_KILL_EXPECTED); } TreeItem *Model::translatePtr(const QModelIndex &index) const { return index.internalPointer() ? static_cast(index.internalPointer()) : m_mailboxes; } QVariant Model::data(const QModelIndex &index, int role) const { if (role == RoleIsNetworkOffline) return !isNetworkAvailable(); return translatePtr(index)->data(const_cast(this), role); } QModelIndex Model::index(int row, int column, const QModelIndex &parent) const { if (parent.isValid()) { Q_ASSERT(parent.model() == this); } TreeItem *parentItem = translatePtr(parent); // Deal with the possibility of an "irregular shape" of our model here. // The issue is that some items have child items not only in column #0 // and in specified number of rows, but also in row #0 and various columns. if (column != 0) { TreeItem *item = parentItem->specialColumnPtr(row, column); if (item) return QAbstractItemModel::createIndex(row, column, item); else return QModelIndex(); } TreeItem *child = parentItem->child(row, const_cast(this)); return child ? QAbstractItemModel::createIndex(row, column, child) : QModelIndex(); } QModelIndex Model::parent(const QModelIndex &index) const { if (!index.isValid()) return QModelIndex(); Q_ASSERT(index.model() == this); TreeItem *childItem = static_cast(index.internalPointer()); TreeItem *parentItem = childItem->parent(); if (! parentItem || parentItem == m_mailboxes) return QModelIndex(); return QAbstractItemModel::createIndex(parentItem->row(), 0, parentItem); } int Model::rowCount(const QModelIndex &index) const { TreeItem *node = static_cast(index.internalPointer()); if (!node) { node = m_mailboxes; } else { Q_ASSERT(index.model() == this); } Q_ASSERT(node); return node->rowCount(const_cast(this)); } int Model::columnCount(const QModelIndex &index) const { TreeItem *node = static_cast(index.internalPointer()); if (!node) { node = m_mailboxes; } else { Q_ASSERT(index.model() == this); } Q_ASSERT(node); return node->columnCount(); } bool Model::hasChildren(const QModelIndex &parent) const { if (parent.isValid()) { Q_ASSERT(parent.model() == this); } TreeItem *node = translatePtr(parent); if (node) return node->hasChildren(const_cast(this)); else return false; } void Model::askForChildrenOfMailbox(const QModelIndex &index, const CacheLoadingMode cacheMode) { TreeItemMailbox *mailbox = 0; if (index.isValid()) { Q_ASSERT(index.model() == this); mailbox = dynamic_cast(static_cast(index.internalPointer())); } else { mailbox = m_mailboxes; } Q_ASSERT(mailbox); askForChildrenOfMailbox(mailbox, cacheMode == LOAD_FORCE_RELOAD); } void Model::askForMessagesInMailbox(const QModelIndex &index) { if (!index.isValid()) return; Q_ASSERT(index.model() == this); auto msgList = dynamic_cast(static_cast(index.internalPointer())); Q_ASSERT(msgList); askForMessagesInMailbox(msgList); } void Model::askForChildrenOfMailbox(TreeItemMailbox *item, bool forceReload) { if (!forceReload && cache()->childMailboxesFresh(item->mailbox())) { // The permanent cache contains relevant data QList metadata = cache()->childMailboxes(item->mailbox()); TreeItemChildrenList mailboxes; for (QList::const_iterator it = metadata.constBegin(); it != metadata.constEnd(); ++it) { TreeItemMailbox *mailbox = TreeItemMailbox::fromMetadata(item, *it); TreeItemMsgList *list = dynamic_cast(mailbox->m_children[0]); Q_ASSERT(list); Imap::Mailbox::SyncState syncState = cache()->mailboxSyncState(mailbox->mailbox()); if (syncState.isUsableForNumbers()) { list->m_unreadMessageCount = syncState.unSeenCount(); list->m_totalMessageCount = syncState.exists(); list->m_recentMessageCount = syncState.recent(); list->m_numberFetchingStatus = TreeItem::LOADING; } else { list->m_numberFetchingStatus = TreeItem::UNAVAILABLE; } mailboxes << mailbox; } TreeItemMailbox *mailboxPtr = dynamic_cast(item); Q_ASSERT(mailboxPtr); replaceChildMailboxes(mailboxPtr, mailboxes); } else if (networkPolicy() == NETWORK_OFFLINE) { // No cached data, no network -> fail item->setFetchStatus(TreeItem::UNAVAILABLE); QModelIndex idx = item->toIndex(this); emit dataChanged(idx, idx); return; } // We shall ask the network m_taskFactory->createListChildMailboxesTask(this, item->toIndex(this)); QModelIndex idx = item->toIndex(this); emit dataChanged(idx, idx); } void Model::reloadMailboxList() { m_mailboxes->rescanForChildMailboxes(this); } void Model::askForMessagesInMailbox(TreeItemMsgList *item) { Q_ASSERT(item->parent()); TreeItemMailbox *mailboxPtr = dynamic_cast(item->parent()); Q_ASSERT(mailboxPtr); QString mailbox = mailboxPtr->mailbox(); Q_ASSERT(item->m_children.size() == 0); auto uidMapping = cache()->uidMapping(mailbox); auto oldSyncState = cache()->mailboxSyncState(mailbox); if (networkPolicy() == NETWORK_OFFLINE && oldSyncState.isUsableForSyncing() && static_cast(uidMapping.size()) != oldSyncState.exists()) { // Problem with the cached data qDebug() << "UID cache stale for mailbox" << mailbox << "(" << uidMapping.size() << "in UID cache vs." << oldSyncState.exists() << "in the sync state and" << item->m_totalMessageCount << "as totalMessageCount (possibly updated by STATUS))"; item->setFetchStatus(TreeItem::UNAVAILABLE); } else if (networkPolicy() == NETWORK_OFFLINE && !oldSyncState.isUsableForSyncing()) { // Nothing in the cache item->setFetchStatus(TreeItem::UNAVAILABLE); } else if (oldSyncState.isUsableForSyncing()) { // We can pre-populate the tree with data from cache Q_ASSERT(item->m_children.isEmpty()); Q_ASSERT(item->accessFetchStatus() == TreeItem::LOADING); QModelIndex listIndex = item->toIndex(this); if (uidMapping.size()) { beginInsertRows(listIndex, 0, uidMapping.size() - 1); for (uint seq = 0; seq < static_cast(uidMapping.size()); ++seq) { TreeItemMessage *message = new TreeItemMessage(item); message->m_offset = seq; message->m_uid = uidMapping[seq]; item->m_children << message; QStringList flags = cache()->msgFlags(mailbox, message->m_uid); flags.removeOne(QStringLiteral("\\Recent")); message->m_flags = normalizeFlags(flags); } endInsertRows(); } mailboxPtr->syncState = oldSyncState; item->setFetchStatus(TreeItem::DONE); // required for FETCH processing later on // The list of messages was satisfied from cache. Do the same for the message counts, if applicable item->recalcVariousMessageCounts(this); } if (networkPolicy() != NETWORK_OFFLINE) { findTaskResponsibleFor(mailboxPtr); // and that's all -- the task will detect following replies and sync automatically } } void Model::askForNumberOfMessages(TreeItemMsgList *item) { Q_ASSERT(item->parent()); TreeItemMailbox *mailboxPtr = dynamic_cast(item->parent()); Q_ASSERT(mailboxPtr); if (networkPolicy() == NETWORK_OFFLINE) { Imap::Mailbox::SyncState syncState = cache()->mailboxSyncState(mailboxPtr->mailbox()); if (syncState.isUsableForNumbers()) { item->m_unreadMessageCount = syncState.unSeenCount(); item->m_totalMessageCount = syncState.exists(); item->m_recentMessageCount = syncState.recent(); item->m_numberFetchingStatus = TreeItem::DONE; emitMessageCountChanged(mailboxPtr); } else { item->m_numberFetchingStatus = TreeItem::UNAVAILABLE; } } else { m_taskFactory->createNumberOfMessagesTask(this, mailboxPtr->toIndex(this)); } } void Model::askForMsgMetadata(TreeItemMessage *item, const PreloadingMode preloadMode) { Q_ASSERT(item->uid()); Q_ASSERT(!item->fetched()); TreeItemMsgList *list = dynamic_cast(item->parent()); Q_ASSERT(list); TreeItemMailbox *mailboxPtr = dynamic_cast(list->parent()); Q_ASSERT(mailboxPtr); if (item->uid()) { AbstractCache::MessageDataBundle data = cache()->messageMetadata(mailboxPtr->mailbox(), item->uid()); if (data.uid == item->uid()) { item->data()->setEnvelope(data.envelope); item->data()->setSize(data.size); item->data()->setHdrReferences(data.hdrReferences); item->data()->setHdrListPost(data.hdrListPost); item->data()->setHdrListPostNo(data.hdrListPostNo); QDataStream stream(&data.serializedBodyStructure, QIODevice::ReadOnly); stream.setVersion(QDataStream::Qt_4_6); QVariantList unserialized; stream >> unserialized; QSharedPointer abstractMessage; try { abstractMessage = Message::AbstractMessage::fromList(unserialized, QByteArray(), 0); } catch (Imap::ParserException &e) { qDebug() << "Error when parsing cached BODYSTRUCTURE" << e.what(); } if (! abstractMessage) { item->setFetchStatus(TreeItem::UNAVAILABLE); } else { auto newChildren = abstractMessage->createTreeItems(item); if (item->m_children.isEmpty()) { TreeItemChildrenList oldChildren = item->setChildren(newChildren); Q_ASSERT(oldChildren.size() == 0); } else { // The following assert guards against that crazy signal emitting we had when various askFor*() // functions were not delayed. If it gets hit, it means that someone tried to call this function // on an item which was already loaded. Q_ASSERT(item->m_children.isEmpty()); item->setChildren(newChildren); } item->setFetchStatus(TreeItem::DONE); } } } switch (networkPolicy()) { case NETWORK_OFFLINE: if (item->accessFetchStatus() != TreeItem::DONE) item->setFetchStatus(TreeItem::UNAVAILABLE); break; case NETWORK_EXPENSIVE: if (item->accessFetchStatus() != TreeItem::DONE) { item->setFetchStatus(TreeItem::LOADING); findTaskResponsibleFor(mailboxPtr)->requestEnvelopeDownload(item->uid()); } break; case NETWORK_ONLINE: { if (item->accessFetchStatus() != TreeItem::DONE) { item->setFetchStatus(TreeItem::LOADING); findTaskResponsibleFor(mailboxPtr)->requestEnvelopeDownload(item->uid()); } // preload if (preloadMode != PRELOAD_PER_POLICY) break; bool ok; int preload = property("trojita-imap-preload-msg-metadata").toInt(&ok); if (! ok) preload = 50; int order = item->row(); for (int i = qMax(0, order - preload); i < qMin(list->m_children.size(), order + preload); ++i) { TreeItemMessage *message = dynamic_cast(list->m_children[i]); Q_ASSERT(message); if (item != message && !message->fetched() && !message->loading() && message->uid()) { message->setFetchStatus(TreeItem::LOADING); // cannot ask the KeepTask directly, that'd completely ignore the cache // but we absolutely have to block the preload :) askForMsgMetadata(message, PRELOAD_DISABLED); } } } break; } EMIT_LATER(this, dataChanged, Q_ARG(QModelIndex, item->toIndex(this)), Q_ARG(QModelIndex, item->toIndex(this))); } void Model::askForMsgPart(TreeItemPart *item, bool onlyFromCache) { Q_ASSERT(item->message()); // TreeItemMessage Q_ASSERT(item->message()->parent()); // TreeItemMsgList Q_ASSERT(item->message()->parent()->parent()); // TreeItemMailbox TreeItemMailbox *mailboxPtr = dynamic_cast(item->message()->parent()->parent()); Q_ASSERT(mailboxPtr); // We are asking for a message part, which means that the structure of a message is already known. // If the UID was zero at this point, it would mean that we are completely doomed. uint uid = static_cast(item->message())->uid(); Q_ASSERT(uid); // Check whether this is a request for fetching the special item representing the raw contents prior to any CTE undoing TreeItemPart *itemForFetchOperation = item; TreeItemModifiedPart *modifiedPart = dynamic_cast(item); bool isSpecialRawPart = modifiedPart && modifiedPart->kind() == TreeItem::OFFSET_RAW_CONTENTS; if (isSpecialRawPart) { itemForFetchOperation = dynamic_cast(item->parent()); Q_ASSERT(itemForFetchOperation); } const QByteArray &data = cache()->messagePart(mailboxPtr->mailbox(), uid, isSpecialRawPart ? itemForFetchOperation->partId() + ".X-RAW" : item->partId()); if (! data.isNull()) { item->m_data = data; item->setFetchStatus(TreeItem::DONE); return; } if (!isSpecialRawPart) { const QByteArray &data = cache()->messagePart(mailboxPtr->mailbox(), uid, itemForFetchOperation->partId() + ".X-RAW"); if (!data.isNull()) { Imap::decodeContentTransferEncoding(data, item->transferEncoding(), item->dataPtr()); item->setFetchStatus(TreeItem::DONE); return; } if (item->m_partRaw && item->m_partRaw->loading()) { // There's already a request for the raw data. Let's use it and don't queue an extra fetch here. item->setFetchStatus(TreeItem::LOADING); return; } } if (networkPolicy() == NETWORK_OFFLINE) { if (item->accessFetchStatus() != TreeItem::DONE) item->setFetchStatus(TreeItem::UNAVAILABLE); } else if (! onlyFromCache) { KeepMailboxOpenTask *keepTask = findTaskResponsibleFor(mailboxPtr); TreeItemPart::PartFetchingMode fetchingMode = TreeItemPart::FETCH_PART_IMAP; if (!isSpecialRawPart && keepTask->parser && accessParser(keepTask->parser).capabilitiesFresh && accessParser(keepTask->parser).capabilities.contains(QStringLiteral("BINARY"))) { if (!item->hasChildren(0) && !item->m_binaryCTEFailed) { // The BINARY only actually makes sense on leaf MIME nodes fetchingMode = TreeItemPart::FETCH_PART_BINARY; } } keepTask->requestPartDownload(item->message()->m_uid, itemForFetchOperation->partIdForFetch(fetchingMode), item->octets()); } } void Model::resyncMailbox(const QModelIndex &mbox) { findTaskResponsibleFor(mbox)->resynchronizeMailbox(); } void Model::setNetworkPolicy(const NetworkPolicy policy) { bool networkReconnected = m_netPolicy == NETWORK_OFFLINE && policy != NETWORK_OFFLINE; switch (policy) { case NETWORK_OFFLINE: for (QMap::iterator it = m_parsers.begin(); it != m_parsers.end(); ++it) { if (!it->parser || it->connState == CONN_STATE_LOGOUT) { // there's no point in sending LOGOUT over these continue; } Q_ASSERT(it->parser); if (it->maintainingTask) { // First of all, give the maintaining task a chance to finish its housekeeping it->maintainingTask->stopForLogout(); } // Kill all tasks that are also using this connection Q_FOREACH(ImapTask *task, it->activeTasks) { task->die(tr("Going offline")); } it->logoutCmd = it->parser->logout(); changeConnectionState(it->parser, CONN_STATE_LOGOUT); } m_netPolicy = NETWORK_OFFLINE; m_periodicMailboxNumbersRefresh->stop(); emit networkPolicyChanged(); emit networkPolicyOffline(); // FIXME: kill the connection break; case NETWORK_EXPENSIVE: m_netPolicy = NETWORK_EXPENSIVE; m_periodicMailboxNumbersRefresh->stop(); emit networkPolicyChanged(); emit networkPolicyExpensive(); break; case NETWORK_ONLINE: m_netPolicy = NETWORK_ONLINE; m_periodicMailboxNumbersRefresh->start(); emit networkPolicyChanged(); emit networkPolicyOnline(); break; } if (networkReconnected) { // We're connecting after being offline if (m_mailboxes->accessFetchStatus() != TreeItem::NONE) { // We should ask for an updated list of mailboxes // The main reason is that this happens after entering wrong password and going back online reloadMailboxList(); } } else if (m_netPolicy == NETWORK_ONLINE) { // The connection is online after some time in a different mode. Let's use this opportunity to request // updated message counts from all visible mailboxes. invalidateAllMessageCounts(); } } void Model::handleSocketDisconnectedResponse(Parser *ptr, const Responses::SocketDisconnectedResponse *const resp) { if (!accessParser(ptr).logoutCmd.isEmpty() || accessParser(ptr).connState == CONN_STATE_LOGOUT) { // If we're already scheduled for logout, don't treat connection errors as, well, errors. // This branch can be reached by e.g. user selecting offline after a network change, with logout // already on the fly. // But we still absolutely want to clean up and kill the connection/Parser anyway killParser(ptr, PARSER_KILL_EXPECTED); } else { logTrace(ptr->parserId(), Common::LOG_PARSE_ERROR, QString(), resp->message); changeConnectionState(ptr, CONN_STATE_LOGOUT); killParser(ptr, PARSER_KILL_EXPECTED); EMIT_LATER(this, networkError, Q_ARG(QString, resp->message)); setNetworkPolicy(NETWORK_OFFLINE); } } void Model::handleParseErrorResponse(Imap::Parser *ptr, const Imap::Responses::ParseErrorResponse *const resp) { Q_ASSERT(ptr); broadcastParseError(ptr->parserId(), resp->exceptionClass, resp->message, resp->line, resp->offset); killParser(ptr, PARSER_KILL_HARD); } void Model::broadcastParseError(const uint parser, const QString &exceptionClass, const QString &errorMessage, const QByteArray &line, int position) { emit logParserFatalError(parser, exceptionClass, errorMessage, line, position); QString details = (position == -1) ? QString() : QString(position, QLatin1Char(' ')) + QLatin1String("^ here"); logTrace(parser, Common::LOG_PARSE_ERROR, exceptionClass, QStringLiteral("%1\n%2\n%3").arg(errorMessage, QString::fromUtf8(line), details)); QString message; if (exceptionClass == QLatin1String("NotAnImapServerError")) { QString service; if (line.startsWith("+OK") || line.startsWith("-ERR")) { service = tr("

    It appears that you are connecting to a POP3 server. That won't work here.

    "); } else if (line.startsWith("220 ") || line.startsWith("220-")) { service = tr("

    It appears that you are connecting to an SMTP server. That won't work here.

    "); } - message = trUtf8("

    This is not an IMAP server

    " + message = tr("

    This is not an IMAP server

    " "%1" "

    Please check your settings to make sure you are connecting to the IMAP service. " "A typical port number for IMAP is 143 or 993.

    " "

    The server said:

    " "
    %2
    ").arg(service, QString::fromUtf8(line)); } else { - message = trUtf8("

    The IMAP server sent us a reply which we could not parse. " + message = tr("

    The IMAP server sent us a reply which we could not parse. " "This might either mean that there's a bug in Trojitá's code, or " "that the IMAP server you are connected to is broken. Please " "report this as a bug anyway. Here are the details:

    " "

    %1: %2

    " "
    %3\n%4
    " ).arg(exceptionClass, errorMessage, QString::fromUtf8(line), details); } EMIT_LATER(this, imapError, Q_ARG(QString, message)); setNetworkPolicy(NETWORK_OFFLINE); } void Model::switchToMailbox(const QModelIndex &mbox) { if (! mbox.isValid()) return; if (m_netPolicy == NETWORK_OFFLINE) return; findTaskResponsibleFor(mbox); } void Model::updateCapabilities(Parser *parser, const QStringList capabilities) { Q_ASSERT(parser); QStringList uppercaseCaps; Q_FOREACH(const QString& str, capabilities) { QString cap = str.toUpper(); if (m_capabilitiesBlacklist.contains(cap)) { logTrace(parser->parserId(), Common::LOG_OTHER, QStringLiteral("Model"), QStringLiteral("Ignoring capability \"%1\"").arg(cap)); continue; } uppercaseCaps << cap; } accessParser(parser).capabilities = uppercaseCaps; accessParser(parser).capabilitiesFresh = true; if (uppercaseCaps.contains(QStringLiteral("LITERAL-"))) { parser->enableLiteralPlus(Parser::LiteralPlus::Minus); } else if (uppercaseCaps.contains(QStringLiteral("LITERAL+"))) { parser->enableLiteralPlus(Parser::LiteralPlus::Plus); } else { parser->enableLiteralPlus(Parser::LiteralPlus::Unsupported); } for (QMap::const_iterator it = m_parsers.constBegin(); it != m_parsers.constEnd(); ++it) { if (it->connState == CONN_STATE_LOGOUT) { // Skip all parsers which are currently stuck in LOGOUT continue; } else { // The CAPABILITIES were received by a first "usable" parser; let's treat this one as the authoritative one emit capabilitiesUpdated(uppercaseCaps); } } if (!uppercaseCaps.contains(QStringLiteral("IMAP4REV1"))) { changeConnectionState(parser, CONN_STATE_LOGOUT); accessParser(parser).logoutCmd = parser->logout(); EMIT_LATER(this, imapError, Q_ARG(QString, tr("We aren't talking to an IMAP4 server"))); setNetworkPolicy(NETWORK_OFFLINE); } } ImapTask *Model::setMessageFlags(const QModelIndexList &messages, const QString flag, const FlagsOperation marked) { Q_ASSERT(!messages.isEmpty()); Q_ASSERT(messages.front().model() == this); return m_taskFactory->createUpdateFlagsTask(this, messages, marked, QLatin1Char('(') + flag + QLatin1Char(')')); } void Model::markMessagesDeleted(const QModelIndexList &messages, const FlagsOperation marked) { this->setMessageFlags(messages, QStringLiteral("\\Deleted"), marked); } void Model::markMailboxAsRead(const QModelIndex &mailbox) { if (!mailbox.isValid()) return; QModelIndex index; realTreeItem(mailbox, 0, &index); Q_ASSERT(index.isValid()); Q_ASSERT(index.model() == this); Q_ASSERT(dynamic_cast(static_cast(index.internalPointer()))); m_taskFactory->createUpdateFlagsOfAllMessagesTask(this, index, Imap::Mailbox::FLAG_ADD_SILENT, QStringLiteral("\\Seen")); } void Model::markMessagesRead(const QModelIndexList &messages, const FlagsOperation marked) { this->setMessageFlags(messages, QStringLiteral("\\Seen"), marked); } ImapTask *Model::copyMoveMessages(const QString &destMailboxName, const QModelIndexList &messages, const CopyMoveOperation op) { return m_taskFactory->createCopyMoveMessagesTask(this, messages, destMailboxName, op); } void Model::copyMoveMessages(TreeItemMailbox *sourceMbox, const QString &destMailboxName, Imap::Uids uids, const CopyMoveOperation op) { if (m_netPolicy == NETWORK_OFFLINE) { // FIXME: error signalling return; } Q_ASSERT(sourceMbox); qSort(uids); QModelIndexList messages; Sequence seq; Q_FOREACH(TreeItemMessage* m, findMessagesByUids(sourceMbox, uids)) { messages << m->toIndex(this); seq.add(m->uid()); } m_taskFactory->createCopyMoveMessagesTask(this, messages, destMailboxName, op); } /** @short Convert a list of UIDs to a list of pointers to the relevant message nodes */ QList Model::findMessagesByUids(const TreeItemMailbox *const mailbox, const Imap::Uids &uids) { const TreeItemMsgList *const list = dynamic_cast(mailbox->m_children[0]); Q_ASSERT(list); QList res; auto it = list->m_children.constBegin(); uint lastUid = 0; Q_FOREACH(const uint uid, uids) { if (lastUid == uid) { // we have to filter out duplicates continue; } lastUid = uid; it = Common::lowerBoundWithUnknownElements(it, list->m_children.constEnd(), uid, messageHasUidZero, uidComparator); if (it != list->m_children.end() && static_cast(*it)->uid() == uid) { res << static_cast(*it); } else { qDebug() << "Can't find UID" << uid; } } return res; } /** @short Find a message with UID that matches the passed key, handling those with UID zero correctly If there's no such message, the next message with a valid UID is returned instead. If there are no such messages, the iterator can point to a message with UID zero or to the end of the list. */ TreeItemChildrenList::iterator Model::findMessageOrNextOneByUid(TreeItemMsgList *list, const uint uid) { return Common::lowerBoundWithUnknownElements(list->m_children.begin(), list->m_children.end(), uid, messageHasUidZero, uidComparator); } TreeItemMailbox *Model::findMailboxByName(const QString &name) const { return findMailboxByName(name, m_mailboxes); } TreeItemMailbox *Model::findMailboxByName(const QString &name, const TreeItemMailbox *const root) const { Q_ASSERT(!root->m_children.isEmpty()); // Names are sorted, so linear search is not required. On the ohterhand, the mailbox sizes are typically small enough // so that this shouldn't matter at all, and linear search is simple enough. for (int i = 1; i < root->m_children.size(); ++i) { TreeItemMailbox *mailbox = static_cast(root->m_children[i]); if (name == mailbox->mailbox()) return mailbox; else if (name.startsWith(mailbox->mailbox() + mailbox->separator())) return findMailboxByName(name, mailbox); } return 0; } /** @short Find a parent mailbox for the specified name */ TreeItemMailbox *Model::findParentMailboxByName(const QString &name) const { TreeItemMailbox *root = m_mailboxes; while (true) { if (root->m_children.size() == 1) { break; } bool found = false; for (int i = 1; !found && i < root->m_children.size(); ++i) { TreeItemMailbox *const item = dynamic_cast(root->m_children[i]); Q_ASSERT(item); if (name.startsWith(item->mailbox() + item->separator())) { root = item; found = true; } } if (!found) { return root; } } return root; } void Model::expungeMailbox(const QModelIndex &mailbox) { if (!mailbox.isValid()) return; if (m_netPolicy == NETWORK_OFFLINE) { qDebug() << "Can't expunge while offline"; return; } m_taskFactory->createExpungeMailboxTask(this, mailbox); } void Model::createMailbox(const QString &name, const AutoSubscription subscription) { if (m_netPolicy == NETWORK_OFFLINE) { qDebug() << "Can't create mailboxes while offline"; return; } auto task = m_taskFactory->createCreateMailboxTask(this, name); if (subscription == AutoSubscription::SUBSCRIBE) { m_taskFactory->createSubscribeUnsubscribeTask(this, task, name, SUBSCRIBE); } } void Model::deleteMailbox(const QString &name) { if (m_netPolicy == NETWORK_OFFLINE) { qDebug() << "Can't delete mailboxes while offline"; return; } m_taskFactory->createDeleteMailboxTask(this, name); } void Model::subscribeMailbox(const QString &name) { if (m_netPolicy == NETWORK_OFFLINE) { qDebug() << "Can't manage subscriptions while offline"; return; } TreeItemMailbox *mailbox = findMailboxByName(name); if (!mailbox) { qDebug() << "SUBSCRIBE: No such mailbox."; return; } m_taskFactory->createSubscribeUnsubscribeTask(this, name, SUBSCRIBE); } void Model::unsubscribeMailbox(const QString &name) { if (m_netPolicy == NETWORK_OFFLINE) { qDebug() << "Can't manage subscriptions while offline"; return; } TreeItemMailbox *mailbox = findMailboxByName(name); if (!mailbox) { qDebug() << "UNSUBSCRIBE: No such mailbox."; return; } m_taskFactory->createSubscribeUnsubscribeTask(this, name, UNSUBSCRIBE); } void Model::saveUidMap(TreeItemMsgList *list) { Imap::Uids seqToUid(list->m_children.size(), 0); std::transform(list->m_children.constBegin(), list->m_children.cend(), seqToUid.begin(), [](TreeItem *item) { return static_cast(item)->uid(); }); cache()->setUidMapping(static_cast(list->parent())->mailbox(), seqToUid); } TreeItem *Model::realTreeItem(QModelIndex index, const Model **whichModel, QModelIndex *translatedIndex) { index = Imap::deproxifiedIndex(index); const Model *model = qobject_cast(index.model()); Q_ASSERT(model); if (whichModel) *whichModel = model; if (translatedIndex) *translatedIndex = index; return static_cast(index.internalPointer()); } void Model::changeConnectionState(Parser *parser, ConnectionState state) { accessParser(parser).connState = state; logTrace(parser->parserId(), Common::LOG_TASKS, QStringLiteral("conn"), connectionStateToString(state)); emit connectionStateChanged(parser->parserId(), state); } void Model::handleSocketStateChanged(Parser *parser, Imap::ConnectionState state) { Q_ASSERT(parser); if (accessParser(parser).connState < state) { changeConnectionState(parser, state); } } void Model::killParser(Parser *parser, ParserKillingMethod method) { if (method == PARSER_JUST_DELETE_LATER) { Q_ASSERT(accessParser(parser).parser == 0); Q_FOREACH(ImapTask *task, accessParser(parser).activeTasks) { task->deleteLater(); } parser->deleteLater(); return; } Q_FOREACH(ImapTask *task, accessParser(parser).activeTasks) { // FIXME: now this message sucks task->die(tr("The connection is being killed for unspecified reason")); } parser->disconnect(); Q_ASSERT(accessParser(parser).parser); accessParser(parser).parser = 0; switch (method) { case PARSER_KILL_EXPECTED: logTrace(parser->parserId(), Common::LOG_IO_WRITTEN, QString(), QStringLiteral("*** Connection closed.")); return; case PARSER_KILL_HARD: logTrace(parser->parserId(), Common::LOG_IO_WRITTEN, QString(), QStringLiteral("*** Connection killed.")); return; case PARSER_JUST_DELETE_LATER: // already handled return; } Q_ASSERT(false); } void Model::slotParserLineReceived(Parser *parser, const QByteArray &line) { logTrace(parser->parserId(), Common::LOG_IO_READ, QString(), QString::fromUtf8(line)); } void Model::slotParserLineSent(Parser *parser, const QByteArray &line) { logTrace(parser->parserId(), Common::LOG_IO_WRITTEN, QString(), QString::fromUtf8(line)); } void Model::setCache(std::shared_ptr cache) { m_cache = cache; } void Model::runReadyTasks() { for (QMap::iterator parserIt = m_parsers.begin(); parserIt != m_parsers.end(); ++parserIt) { bool runSomething = false; do { runSomething = false; // See responseReceived() for more details about why we do need to iterate over a copy here. // Basically, calls to ImapTask::perform could invalidate our precious iterators. QList origList = parserIt->activeTasks; QList deletedList; QList::const_iterator taskEnd = origList.constEnd(); for (QList::const_iterator taskIt = origList.constBegin(); taskIt != taskEnd; ++taskIt) { ImapTask *task = *taskIt; if (task->isReadyToRun()) { task->perform(); runSomething = true; } if (task->isFinished()) { deletedList << task; } } removeDeletedTasks(deletedList, parserIt->activeTasks); #ifdef TROJITA_DEBUG_TASK_TREE if (!deletedList.isEmpty()) checkTaskTreeConsistency(); #endif } while (runSomething); } } void Model::removeDeletedTasks(const QList &deletedTasks, QList &activeTasks) { // Remove the finished commands for (QList::const_iterator deletedIt = deletedTasks.begin(); deletedIt != deletedTasks.end(); ++deletedIt) { (*deletedIt)->deleteLater(); activeTasks.removeOne(*deletedIt); // It isn't destroyed yet, but should be removed from the model nonetheless m_taskModel->slotSomeTaskDestroyed(); } } KeepMailboxOpenTask *Model::findTaskResponsibleFor(const QModelIndex &mailbox) { Q_ASSERT(mailbox.isValid()); QModelIndex translatedIndex; TreeItemMailbox *mailboxPtr = dynamic_cast(realTreeItem(mailbox, 0, &translatedIndex)); return findTaskResponsibleFor(mailboxPtr); } KeepMailboxOpenTask *Model::findTaskResponsibleFor(TreeItemMailbox *mailboxPtr) { Q_ASSERT(mailboxPtr); bool canCreateParallelConn = m_parsers.isEmpty(); // FIXME: multiple connections if (mailboxPtr->maintainingTask) { // The requested mailbox already has the maintaining task associated if (accessParser(mailboxPtr->maintainingTask->parser).connState == CONN_STATE_LOGOUT) { // The connection is currently getting closed, so we have to create another one return m_taskFactory->createKeepMailboxOpenTask(this, mailboxPtr->toIndex(this), 0); } else { // it's usable as-is return mailboxPtr->maintainingTask; } } else if (canCreateParallelConn) { // The mailbox is not being maintained, but we can create a new connection return m_taskFactory->createKeepMailboxOpenTask(this, mailboxPtr->toIndex(this), 0); } else { // Too bad, we have to re-use an existing parser. That will probably lead to // stealing it from some mailbox, but there's no other way. Q_ASSERT(!m_parsers.isEmpty()); for (QMap::const_iterator it = m_parsers.constBegin(); it != m_parsers.constEnd(); ++it) { if (it->connState == CONN_STATE_LOGOUT) { // this one is not usable continue; } return m_taskFactory->createKeepMailboxOpenTask(this, mailboxPtr->toIndex(this), it.key()); } // At this point, we have no other choice than to create a new connection return m_taskFactory->createKeepMailboxOpenTask(this, mailboxPtr->toIndex(this), 0); } } void Model::genericHandleFetch(TreeItemMailbox *mailbox, const Imap::Responses::Fetch *const resp) { Q_ASSERT(mailbox); QList changedParts; TreeItemMessage *changedMessage = 0; mailbox->handleFetchResponse(this, *resp, changedParts, changedMessage, false); if (! changedParts.isEmpty()) { Q_FOREACH(TreeItemPart* part, changedParts) { QModelIndex index = part->toIndex(this); emit dataChanged(index, index); } } if (changedMessage) { QModelIndex index = changedMessage->toIndex(this); emit dataChanged(index, index); emitMessageCountChanged(mailbox); } } QModelIndex Model::findMailboxForItems(const QModelIndexList &items) { TreeItemMailbox *mailbox = 0; Q_FOREACH(const QModelIndex& index, items) { TreeItemMailbox *currentMailbox = 0; Q_ASSERT(index.model() == this); TreeItem *item = static_cast(index.internalPointer()); Q_ASSERT(item); if ((currentMailbox = dynamic_cast(item))) { // yes, that's an assignment, not a comparison // This case is OK } else { // TreeItemMessage and TreeItemPart have to walk the tree, which is why they are lumped together in this branch TreeItemMessage *message = dynamic_cast(item); if (!message) { if (TreeItemPart *part = dynamic_cast(item)) { message = part->message(); } else { throw CantHappen("findMailboxForItems() called on strange items"); } } Q_ASSERT(message); TreeItemMsgList *list = dynamic_cast(message->parent()); Q_ASSERT(list); currentMailbox = dynamic_cast(list->parent()); } Q_ASSERT(currentMailbox); if (!mailbox) { mailbox = currentMailbox; } else if (mailbox != currentMailbox) { throw CantHappen("Messages from several mailboxes"); } } return mailbox->toIndex(this); } void Model::slotTasksChanged() { dumpModelContents(m_taskModel); } void Model::slotTaskDying(QObject *obj) { std::for_each(m_parsers.begin(), m_parsers.end(), [obj](ParserState &state) { state.activeTasks.removeOne(reinterpret_cast(obj)); }); m_taskModel->slotSomeTaskDestroyed(); } TreeItemMailbox *Model::mailboxForSomeItem(QModelIndex index) { TreeItemMailbox *mailbox = dynamic_cast(static_cast(index.internalPointer())); while (index.isValid() && ! mailbox) { index = index.parent(); mailbox = dynamic_cast(static_cast(index.internalPointer())); } return mailbox; } ParserState &Model::accessParser(Parser *parser) { Q_ASSERT(m_parsers.contains(parser)); return m_parsers[ parser ]; } void Model::releaseMessageData(const QModelIndex &message) { if (! message.isValid()) return; const Model *whichModel = 0; QModelIndex realMessage; realTreeItem(message, &whichModel, &realMessage); Q_ASSERT(whichModel == this); TreeItemMessage *msg = dynamic_cast(static_cast(realMessage.internalPointer())); if (! msg) return; msg->setFetchStatus(TreeItem::NONE); #ifndef XTUPLE_CONNECT beginRemoveRows(realMessage, 0, msg->m_children.size() - 1); #endif if (msg->data()->partHeader()) { msg->data()->partHeader()->silentlyReleaseMemoryRecursive(); msg->data()->setPartHeader(nullptr); } if (msg->data()->partText()) { msg->data()->partText()->silentlyReleaseMemoryRecursive(); msg->data()->setPartText(nullptr); } delete msg->m_data; msg->m_data = 0; Q_FOREACH(TreeItem *item, msg->m_children) { TreeItemPart *part = dynamic_cast(item); Q_ASSERT(part); part->silentlyReleaseMemoryRecursive(); delete part; } msg->m_children.clear(); #ifndef XTUPLE_CONNECT endRemoveRows(); emit dataChanged(realMessage, realMessage); #endif } QStringList Model::capabilities() const { if (m_parsers.isEmpty()) return QStringList(); if (m_parsers.constBegin()->capabilitiesFresh) return m_parsers.constBegin()->capabilities; return QStringList(); } void Model::logTrace(uint parserId, const Common::LogKind kind, const QString &source, const QString &message) { Common::LogMessage m(QDateTime::currentDateTime(), kind, source, message, 0); emit logged(parserId, m); } /** @short Overloaded version which accepts a QModelIndex of an item which is somehow "related" to the logged message The relevantIndex argument is used for finding out what parser to send the message to. */ void Model::logTrace(const QModelIndex &relevantIndex, const Common::LogKind kind, const QString &source, const QString &message) { Q_ASSERT(relevantIndex.isValid()); QModelIndex translatedIndex; realTreeItem(relevantIndex, 0, &translatedIndex); // It appears that it's OK to use 0 here; the attached loggers apparently deal with random parsers appearing just OK uint parserId = 0; if (translatedIndex.isValid()) { Q_ASSERT(translatedIndex.model() == this); QModelIndex mailboxIndex = findMailboxForItems(QModelIndexList() << translatedIndex); Q_ASSERT(mailboxIndex.isValid()); TreeItemMailbox *mailboxPtr = dynamic_cast(static_cast(mailboxIndex.internalPointer())); Q_ASSERT(mailboxPtr); if (mailboxPtr->maintainingTask) { parserId = mailboxPtr->maintainingTask->parser->parserId(); } } logTrace(parserId, kind, source, message); } QAbstractItemModel *Model::taskModel() const { return m_taskModel; } QMap Model::serverId() const { return m_idResult; } /** @short Handle explicit sharing and case mapping for message flags This function will try to minimize the amount of QString instances used for storage of individual message flags via Qt's implicit sharing that is built into QString. At the same time, some well-known flags are converted to their "canonical" form (like \\SEEN -> \\Seen etc). */ QStringList Model::normalizeFlags(const QStringList &source) const { QStringList res; res.reserve(source.size()); for (QStringList::const_iterator flag = source.constBegin(); flag != source.constEnd(); ++flag) { // At first, perform a case-insensitive lookup in the (rather short) list of known special flags // Only call the toLower for flags which could possibly be in that mapping. Looking at the first letter is // a good approximation. if (!flag->isEmpty() && ((*flag)[0] == QLatin1Char('\\') || (*flag)[0] == QLatin1Char('$'))) { QString lowerCase = flag->toLower(); QHash::const_iterator known = FlagNames::toCanonical.constFind(lowerCase); if (known != FlagNames::toCanonical.constEnd()) { res.append(*known); continue; } } // If it isn't a special flag, just check whether it's been encountered already QSet::const_iterator it = m_flagLiterals.constFind(*flag); if (it == m_flagLiterals.constEnd()) { // Not in cache, so add it and return an implicitly shared copy m_flagLiterals.insert(*flag); res.append(*flag); } else { // It's in the cache already, se let's QString share the data res.append(*it); } } // Always sort the flags when performing normalization to obtain reasonable results and be ready for possible future // deduplication of the actual QLists res.sort(); return res; } /** @short Set the IMAP username */ void Model::setImapUser(const QString &imapUser) { m_imapUser = imapUser; } /** @short Username to use for login */ QString Model::imapUser() const { return m_imapUser; } /** @short Set the password that the user wants to use */ void Model::setImapPassword(const QString &password) { m_imapPassword = password; m_hasImapPassword = PasswordAvailability::AVAILABLE; informTasksAboutNewPassword(); } /** @short Return the user's password, if cached */ QString Model::imapPassword() const { return m_imapPassword; } /** @short Indicate that the user doesn't want to provide her password */ void Model::unsetImapPassword() { m_imapPassword.clear(); m_hasImapPassword = PasswordAvailability::NOT_REQUESTED; informTasksAboutNewPassword(); } QString Model::imapAuthError() const { return m_imapAuthError; } void Model::setImapAuthError(const QString &error) { if (m_imapAuthError == error) return; m_imapAuthError = error; emit imapAuthErrorChanged(error); } /** @short Tell all tasks which want to know about the availability of a password */ void Model::informTasksAboutNewPassword() { Q_FOREACH(const ParserState &p, m_parsers) { Q_FOREACH(ImapTask *task, p.activeTasks) { OpenConnectionTask *openTask = dynamic_cast(task); if (!openTask) continue; openTask->authCredentialsNowAvailable(); } } } /** @short Forward a policy decision about accepting or rejecting a SSL state */ void Model::setSslPolicy(const QList &sslChain, const QList &sslErrors, bool proceed) { if (proceed) { // Only remember positive values; there is no point in blocking any further connections until settings reload m_sslErrorPolicy.prepend(qMakePair(qMakePair(sslChain, sslErrors), proceed)); } Q_FOREACH(const ParserState &p, m_parsers) { Q_FOREACH(ImapTask *task, p.activeTasks) { OpenConnectionTask *openTask = dynamic_cast(task); if (!openTask) continue; if (openTask->sslCertificateChain() == sslChain && openTask->sslErrors() == sslErrors) { openTask->sslConnectionPolicyDecided(proceed); } } } } void Model::processSslErrors(OpenConnectionTask *task) { // Qt doesn't define either operator< or a qHash specialization for QList (what a surprise), // so we use a plain old QList. Given that there will be at most one different QList sequence for // each connection attempt (and more realistically, for each server at all), this O(n) complexity shall not matter // at all. QList, QList >, bool> >::const_iterator it = m_sslErrorPolicy.constBegin(); while (it != m_sslErrorPolicy.constEnd()) { if (it->first.first == task->sslCertificateChain() && it->first.second == task->sslErrors()) { task->sslConnectionPolicyDecided(it->second); return; } ++it; } EMIT_LATER(this, needsSslDecision, Q_ARG(QList, task->sslCertificateChain()), Q_ARG(QList, task->sslErrors())); } QModelIndex Model::messageIndexByUid(const QString &mailboxName, const uint uid) { TreeItemMailbox *mailbox = findMailboxByName(mailboxName); Q_ASSERT(mailbox); QList messages = findMessagesByUids(mailbox, Imap::Uids() << uid); if (messages.isEmpty()) { return QModelIndex(); } else { Q_ASSERT(messages.size() == 1); return messages.front()->toIndex(this); } } /** @short Forget any cached data about number of messages in all mailboxes */ void Model::invalidateAllMessageCounts() { QList queue; queue.append(m_mailboxes); while (!queue.isEmpty()) { TreeItemMailbox *head = queue.takeFirst(); // ignore first child, the TreeItemMsgList for (auto it = head->m_children.constBegin() + 1; it != head->m_children.constEnd(); ++it) { queue.append(static_cast(*it)); } TreeItemMsgList *list = dynamic_cast(head->m_children[0]); if (list->m_numberFetchingStatus == TreeItem::DONE && !head->maintainingTask) { // Ask only for data which were previously available // Also don't mess with a mailbox which is already being kept up-to-date because it's selected. list->m_numberFetchingStatus = TreeItem::NONE; emitMessageCountChanged(head); } } } AppendTask *Model::appendIntoMailbox(const QString &mailbox, const QByteArray &rawMessageData, const QStringList &flags, const QDateTime ×tamp) { return m_taskFactory->createAppendTask(this, mailbox, rawMessageData, flags, timestamp); } AppendTask *Model::appendIntoMailbox(const QString &mailbox, const QList &data, const QStringList &flags, const QDateTime ×tamp) { return m_taskFactory->createAppendTask(this, mailbox, data, flags, timestamp); } GenUrlAuthTask *Model::generateUrlAuthForMessage(const QString &host, const QString &user, const QString &mailbox, const uint uidValidity, const uint uid, const QString &part, const QString &access) { return m_taskFactory->createGenUrlAuthTask(this, host, user, mailbox, uidValidity, uid, part, access); } UidSubmitTask *Model::sendMailViaUidSubmit(const QString &mailbox, const uint uidValidity, const uint uid, const UidSubmitOptionsList &options) { return m_taskFactory->createUidSubmitTask(this, mailbox, uidValidity, uid, options); } #ifdef TROJITA_DEBUG_TASK_TREE #define TROJITA_DEBUG_TASK_TREE_VERBOSE void Model::checkTaskTreeConsistency() { for (QMap::const_iterator parserIt = m_parsers.constBegin(); parserIt != m_parsers.constEnd(); ++parserIt) { #ifdef TROJITA_DEBUG_TASK_TREE_VERBOSE qDebug() << "\nParser" << parserIt.key() << "; all active tasks:"; Q_FOREACH(ImapTask *activeTask, parserIt.value().activeTasks) { qDebug() << ' ' << activeTask << activeTask->debugIdentification() << activeTask->parser; } #endif Q_FOREACH(ImapTask *activeTask, parserIt.value().activeTasks) { #ifdef TROJITA_DEBUG_TASK_TREE_VERBOSE qDebug() << "Active task" << activeTask << activeTask->debugIdentification() << activeTask->parser; #endif Q_ASSERT(activeTask->parser == parserIt.key()); Q_ASSERT(!activeTask->parentTask); checkDependentTasksConsistency(parserIt.key(), activeTask, 0, 0); } // Make sure that no task is present twice in here QList taskQueue = parserIt.value().activeTasks; for (int i = 0; i < taskQueue.size(); ++i) { Q_FOREACH(ImapTask *yetAnotherTask, taskQueue[i]->dependentTasks) { Q_ASSERT(!taskQueue.contains(yetAnotherTask)); taskQueue.push_back(yetAnotherTask); } } } } void Model::checkDependentTasksConsistency(Parser *parser, ImapTask *task, ImapTask *expectedParentTask, int depth) { #ifdef TROJITA_DEBUG_TASK_TREE_VERBOSE QByteArray prefix; prefix.fill(' ', depth); qDebug() << prefix.constData() << "Checking" << task << task->debugIdentification(); #endif Q_ASSERT(parser); Q_ASSERT(!task->parser || task->parser == parser); Q_ASSERT(task->parentTask == expectedParentTask); if (task->parentTask) { Q_ASSERT(task->parentTask->dependentTasks.contains(task)); if (task->parentTask->parentTask) { Q_ASSERT(task->parentTask->parentTask->dependentTasks.contains(task->parentTask)); } else { Q_ASSERT(task->parentTask->parser); Q_ASSERT(accessParser(task->parentTask->parser).activeTasks.contains(task->parentTask)); } } else { Q_ASSERT(accessParser(parser).activeTasks.contains(task)); } Q_FOREACH(ImapTask *childTask, task->dependentTasks) { checkDependentTasksConsistency(parser, childTask, task, depth + 1); } } #endif void Model::setCapabilitiesBlacklist(const QStringList &blacklist) { m_capabilitiesBlacklist = blacklist; } bool Model::isCatenateSupported() const { return capabilities().contains(QStringLiteral("CATENATE")); } bool Model::isGenUrlAuthSupported() const { return capabilities().contains(QStringLiteral("URLAUTH")); } bool Model::isImapSubmissionSupported() const { QStringList caps = capabilities(); return caps.contains(QStringLiteral("UIDPLUS")) && caps.contains(QStringLiteral("X-DRAFT-I01-SENDMAIL")); } void Model::setNumberRefreshInterval(const int interval) { if (interval == m_periodicMailboxNumbersRefresh->interval()) return; // QTimer does not check idempotency m_periodicMailboxNumbersRefresh->start(interval * 1000); } } } diff --git a/src/Plugins/ClearTextPassword/ClearTextPassword.cpp b/src/Plugins/ClearTextPassword/ClearTextPassword.cpp index 5846877b..22d6b5fd 100644 --- a/src/Plugins/ClearTextPassword/ClearTextPassword.cpp +++ b/src/Plugins/ClearTextPassword/ClearTextPassword.cpp @@ -1,132 +1,132 @@ /* 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 "ClearTextPassword.h" struct Settings { static QString imapPassKey, smtpPassKey; }; QString Settings::imapPassKey = QStringLiteral("imap.auth.pass"); QString Settings::smtpPassKey = QStringLiteral("msa.smtp.auth.pass"); ClearTextPasswordJob::ClearTextPasswordJob(const QString &accountId, const QString &accountType, const QString &password, enum Type type, QObject *parent, QSettings *settings) : PasswordJob(parent), m_accountId(accountId), m_accountType(accountType), m_password(password), m_type(type), m_settings(settings) { } void ClearTextPasswordJob::doStart() { QVariant password; switch (m_type) { case Request: if (m_accountType == QLatin1String("imap")) { password = m_settings->value(Settings::imapPassKey); } else if (m_accountType == QLatin1String("smtp")) { password = m_settings->value(Settings::smtpPassKey); } else { emit error(PasswordJob::UnknownError, tr("This plugin only supports storing of IMAP and SMTP passwords")); break; } if (password.type() != QVariant::String || password.toString().isEmpty()) { emit error(PasswordJob::NoSuchPassword, QString()); } else { emit passwordAvailable(password.toString()); } break; case Store: if (m_accountType == QLatin1String("imap")) { m_settings->setValue(Settings::imapPassKey, m_password); } else if (m_accountType == QLatin1String("smtp")) { m_settings->setValue(Settings::smtpPassKey, m_password); } else { emit error(PasswordJob::UnknownError, tr("This plugin only supports storing of IMAP and SMTP passwords")); break; } emit passwordStored(); break; case Delete: if (m_accountType == QLatin1String("imap")) { m_settings->remove(Settings::imapPassKey); } else if (m_accountType == QLatin1String("smtp")) { m_settings->remove(Settings::smtpPassKey); } else { emit error(PasswordJob::UnknownError, tr("This plugin only supports storing of IMAP and SMTP passwords")); break; } emit passwordDeleted(); break; } } void ClearTextPasswordJob::doStop() { emit error(PasswordJob::Stopped, QString()); } ClearTextPassword::ClearTextPassword(QObject *parent, QSettings *settings) : PasswordPlugin(parent), m_settings(settings) { } PasswordPlugin::Features ClearTextPassword::features() const { return 0; } PasswordJob *ClearTextPassword::requestPassword(const QString &accountId, const QString &accountType) { return new ClearTextPasswordJob(accountId, accountType, QString(), ClearTextPasswordJob::Request, this, m_settings); } PasswordJob *ClearTextPassword::storePassword(const QString &accountId, const QString &accountType, const QString &password) { return new ClearTextPasswordJob(accountId, accountType, password, ClearTextPasswordJob::Store, this, m_settings); } PasswordJob *ClearTextPassword::deletePassword(const QString &accountId, const QString &accountType) { return new ClearTextPasswordJob(accountId, accountType, QString(), ClearTextPasswordJob::Delete, this, m_settings); } QString trojita_plugin_ClearTextPasswordPlugin::name() const { return QStringLiteral("cleartextpassword"); } QString trojita_plugin_ClearTextPasswordPlugin::description() const { - return trUtf8("Trojitá's settings"); + return tr("Trojitá's settings"); } Plugins::PasswordPlugin *trojita_plugin_ClearTextPasswordPlugin::create(QObject *parent, QSettings *settings) { Q_ASSERT(settings); return new ClearTextPassword(parent, settings); } // vim: set et ts=4 sts=4 sw=4 diff --git a/src/Plugins/SonnetSpellchecker/SonnetSpellchecker.cpp b/src/Plugins/SonnetSpellchecker/SonnetSpellchecker.cpp index 9b513c4b..32321249 100644 --- a/src/Plugins/SonnetSpellchecker/SonnetSpellchecker.cpp +++ b/src/Plugins/SonnetSpellchecker/SonnetSpellchecker.cpp @@ -1,55 +1,55 @@ /* Copyright (C) Jan Kundrát 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 "SonnetSpellchecker.h" #include #include namespace Plugins { SonnetSpellchecker::SonnetSpellchecker(QObject *parent) : SpellcheckerPlugin(parent) { } void SonnetSpellchecker::actOnEditor(QTextEdit *editor) { Sonnet::SpellCheckDecorator *decorator = new Sonnet::SpellCheckDecorator(editor); decorator->highlighter()->setAutomatic(false); } QString trojita_plugin_SonnetSpellcheckerPlugin::name() const { return QStringLiteral("sonnetspellchecker"); } QString trojita_plugin_SonnetSpellcheckerPlugin::description() const { - return trUtf8("Sonnet (KF5)"); + return tr("Sonnet (KF5)"); } Plugins::SpellcheckerPlugin *trojita_plugin_SonnetSpellcheckerPlugin::create(QObject *parent, QSettings *settings) { Q_UNUSED(settings); return new SonnetSpellchecker(parent); } }