Commit 3a41f765 authored by Marco Rebhan's avatar Marco Rebhan Committed by Laurent Montel
Browse files

Add Face header support

parent c44e8d91
Pipeline #83199 passed with stage
in 25 minutes and 26 seconds
......@@ -46,7 +46,7 @@ set(KDEPIM_VERSION "${PIM_VERSION}${KDEPIM_DEV_VERSION} (${RELEASE_SERVICE_VERSI
set(AKONADI_MIMELIB_VERSION "5.18.40")
set(AKONADI_CONTACT_VERSION "5.18.40")
set(CALENDARUTILS_LIB_VERSION "5.18.40")
set(IDENTITYMANAGEMENT_LIB_VERSION "5.18.40")
set(IDENTITYMANAGEMENT_LIB_VERSION "5.18.43")
set(KLDAP_LIB_VERSION "5.18.40")
set(KMAILTRANSPORT_LIB_VERSION "5.18.40")
set(KONTACTINTERFACE_LIB_VERSION "5.18.40")
......
......@@ -75,6 +75,7 @@ target_sources(kmailprivate PRIVATE
identity/identitylistview.cpp
identity/identitydialog.cpp
identity/xfaceconfigurator.cpp
identity/encodedimagepicker.cpp
identity/identitypage.cpp
identity/newidentitydialog.cpp
identity/identityeditvcarddialog.cpp
......@@ -221,6 +222,8 @@ ki18n_wrap_ui(kmailprivate
ui/accountspagereceivingtab.ui
ui/searchwindow.ui
ui/incompleteindexdialog.ui
ui/xfaceconfigurator.ui
ui/encodedimagepicker.ui
)
# KCFG files. The main kmail.kcfg is configured by CMake and put
......
......@@ -1650,7 +1650,7 @@ uint KMComposerWin::currentIdentity() const
return mComposerBase->identityCombo()->currentIdentity();
}
void KMComposerWin::addXFace(const KIdentityManagement::Identity &ident, const KMime::Message::Ptr &msg)
void KMComposerWin::addFaceHeaders(const KIdentityManagement::Identity &ident, const KMime::Message::Ptr &msg)
{
if (!ident.isXFaceEnabled() || ident.xface().isEmpty()) {
msg->removeHeader("X-Face");
......@@ -1666,6 +1666,30 @@ void KMComposerWin::addXFace(const KIdentityManagement::Identity &ident, const K
msg->setHeader(header);
}
}
if (!ident.isFaceEnabled() || ident.face().isEmpty()) {
msg->removeHeader("Face");
} else {
QString face = ident.face();
if (!face.isEmpty()) {
// The first line of data is 72 lines long to account for the
// header name, the following lines are 76 lines long, like in
// https://quimby.gnus.org/circus/face/
if (face.length() > 72) {
int numNL = (face.length() - 73) / 76;
for (int i = numNL; i > 0; --i) {
face.insert(72 + i * 76, QStringLiteral("\n\t"));
}
face.insert(72, QStringLiteral("\n\t"));
}
auto header = new KMime::Headers::Generic("Face");
header->fromUnicodeString(face, "utf-8");
msg->setHeader(header);
}
}
}
void KMComposerWin::setMessage(const KMime::Message::Ptr &newMsg,
......@@ -1783,7 +1807,7 @@ void KMComposerWin::setMessage(const KMime::Message::Ptr &newMsg,
const auto &ident = identity();
addXFace(ident, mMsg);
addFaceHeaders(ident, mMsg);
// if these headers are present, the state of the message should be overruled
if (auto hdr = mMsg->headerByType("X-KMail-SignatureActionEnabled")) {
......@@ -3238,7 +3262,8 @@ void KMComposerWin::slotIdentityChanged(uint uoid, bool initialChange)
organization->fromUnicodeString(ident.organization(), "utf-8");
mMsg->setHeader(organization);
}
addXFace(ident, mMsg);
addFaceHeaders(ident, mMsg);
if (initialChange) {
if (auto hrd = mMsg->headerByType("X-KMail-Transport")) {
......
......@@ -582,7 +582,7 @@ private:
Q_REQUIRED_RESULT bool sendLaterRegistered() const;
void slotRecipientEditorLineFocused();
void updateHamburgerMenu();
void addXFace(const KIdentityManagement::Identity &ident, const KMime::Message::Ptr &msg);
void addFaceHeaders(const KIdentityManagement::Identity &ident, const KMime::Message::Ptr &msg);
Akonadi::Collection mCollectionForNewMessage;
QMap<QByteArray, QString> mExtraHeaders;
......
/*
encodedimagepicker.cpp
KMail, the KDE mail client.
SPDX-FileCopyrightText: 2021 the KMail authors.
See file AUTHORS for details
SPDX-License-Identifier: GPL-2.0-only
*/
#include "encodedimagepicker.h"
#include "ui_encodedimagepicker.h"
#include <Akonadi/Contact/ContactSearchJob>
#include <KIdentityManagement/Identity>
#include <KIdentityManagement/IdentityManager>
#include <KPIMTextEdit/PlainTextEditor>
#include <KIO/StoredTransferJob>
#include <KJobWidgets>
#include <KLocalizedString>
#include <KMessageBox>
#include <QFileDialog>
#include <QImageReader>
using namespace KMail;
using KContacts::Addressee;
EncodedImagePicker::EncodedImagePicker(QWidget *parent)
: QGroupBox(parent)
, mUi(new Ui::EncodedImagePicker)
{
mUi->setupUi(this);
mUi->source->editor()->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
// wrap anywhere, these contain encoded data
mUi->source->editor()->setWordWrapMode(QTextOption::WrapAnywhere);
connect(mUi->openImageButton, &QAbstractButton::clicked, this, &EncodedImagePicker::selectFile);
connect(mUi->selectContactsButton, &QPushButton::released, this, &EncodedImagePicker::selectFromAddressBook);
connect(mUi->source->editor(), &KPIMTextEdit::PlainTextEditor::textChanged, this, &EncodedImagePicker::sourceChanged);
}
EncodedImagePicker::~EncodedImagePicker() = default;
void EncodedImagePicker::setInfo(const QString &info)
{
mUi->infoLabel->setText(info);
}
QString EncodedImagePicker::source() const
{
return mUi->source->editor()->toPlainText();
}
void EncodedImagePicker::setSource(const QString &source)
{
mUi->source->editor()->setPlainText(source);
}
void EncodedImagePicker::setImage(const QImage &image)
{
if (image.isNull()) {
mUi->image->clear();
} else {
const QPixmap p = QPixmap::fromImage(image);
mUi->image->setPixmap(p);
}
}
void EncodedImagePicker::selectFile()
{
QString filter;
const QList<QByteArray> supportedImage = QImageReader::supportedImageFormats();
for (const QByteArray &ba : supportedImage) {
if (!filter.isEmpty()) {
filter += QLatin1Char(' ');
}
filter += QLatin1String("*.") + QString::fromLatin1(ba);
}
filter = QStringLiteral("%1 (%2)").arg(i18n("Image"), filter);
const QUrl url = QFileDialog::getOpenFileUrl(this, QString(), QUrl(), filter);
if (!url.isEmpty()) {
setFromFile(url);
}
}
void EncodedImagePicker::setFromFile(const QUrl &url)
{
auto job = KIO::storedGet(url);
KJobWidgets::setWindow(job, this);
connect(job, &KJob::result, this, &EncodedImagePicker::setFromFileDone);
job->start();
}
void EncodedImagePicker::setFromFileDone(KJob *job)
{
const KIO::StoredTransferJob *kioJob = qobject_cast<KIO::StoredTransferJob *>(job);
if (kioJob->error() == 0) {
const QImage image = QImage::fromData(kioJob->data());
Q_EMIT imageSelected(image);
} else {
KMessageBox::error(this, kioJob->errorString());
}
}
void EncodedImagePicker::selectFromAddressBook()
{
using namespace KIdentityManagement;
const IdentityManager manager(true);
const Identity defaultIdentity = manager.defaultIdentity();
const QString email = defaultIdentity.primaryEmailAddress().toLower();
auto job = new Akonadi::ContactSearchJob(this);
job->setLimit(1);
job->setQuery(Akonadi::ContactSearchJob::Email, email, Akonadi::ContactSearchJob::ExactMatch);
connect(job, &KJob::result, this, &EncodedImagePicker::selectFromAddressBookDone);
}
void EncodedImagePicker::selectFromAddressBookDone(KJob *job)
{
const Akonadi::ContactSearchJob *searchJob = qobject_cast<Akonadi::ContactSearchJob *>(job);
if (searchJob->contacts().isEmpty()) {
KMessageBox::information(this, i18n("You do not have your own contact defined in the address book."), i18n("No Picture"));
return;
}
const Addressee contact = searchJob->contacts().at(0);
if (contact.photo().isIntern()) {
const QImage photo = contact.photo().data();
if (photo.isNull()) {
KMessageBox::information(this, i18n("No picture set for your address book entry."), i18n("No Picture"));
} else {
Q_EMIT imageSelected(photo);
}
} else {
const QUrl url(contact.photo().url());
if (url.isEmpty()) {
KMessageBox::information(this, i18n("No picture set for your address book entry."), i18n("No Picture"));
} else {
setFromFile(url);
}
}
}
/* -*- c++ -*-
encodedimagepicker.h
KMail, the KDE mail client.
SPDX-FileCopyrightText: 2021 the KMail authors.
See file AUTHORS for details
SPDX-License-Identifier: GPL-2.0-only
*/
#pragma once
#include <QGroupBox>
class KJob;
namespace Ui
{
class EncodedImagePicker;
}
namespace KMail
{
class EncodedImagePicker : public QGroupBox
{
Q_OBJECT
public:
explicit EncodedImagePicker(QWidget *parent = nullptr);
~EncodedImagePicker() override;
void setInfo(const QString &info);
Q_REQUIRED_RESULT QString source() const;
void setSource(const QString &source);
void setImage(const QImage &image);
Q_SIGNALS:
void imageSelected(const QImage &);
void sourceChanged();
private:
void setFromFile(const QUrl &url);
void selectFile();
void setFromFileDone(KJob *);
void selectFromAddressBook();
void selectFromAddressBookDone(KJob *);
private:
QScopedPointer<Ui::EncodedImagePicker> mUi;
};
} // namespace KMail
......@@ -952,6 +952,8 @@ void IdentityDialog::setIdentity(KIdentityManagement::Identity &ident)
mSignatureConfigurator->setSignature(ident.signature());
mXFaceConfigurator->setXFace(ident.xface());
mXFaceConfigurator->setXFaceEnabled(ident.isXFaceEnabled());
mXFaceConfigurator->setFace(ident.face());
mXFaceConfigurator->setFaceEnabled(ident.isFaceEnabled());
}
void IdentityDialog::unregisterSpecialCollection(qint64 colId)
......@@ -1064,6 +1066,8 @@ void IdentityDialog::updateIdentity(KIdentityManagement::Identity &ident)
ident.setSignature(mSignatureConfigurator->signature());
ident.setXFace(mXFaceConfigurator->xface());
ident.setXFaceEnabled(mXFaceConfigurator->isXFaceEnabled());
ident.setFace(mXFaceConfigurator->face());
ident.setFaceEnabled(mXFaceConfigurator->isFaceEnabled());
}
void IdentityDialog::slotEditVcard()
......
......@@ -7,252 +7,266 @@
*/
#include "xfaceconfigurator.h"
#include "encodedimagepicker.h"
#include "ui_xfaceconfigurator.h"
#include <Akonadi/Contact/ContactSearchJob>
#include <KIdentityManagement/Identity>
#include <KIdentityManagement/IdentityManager>
#include <KPIMTextEdit/PlainTextEditor>
#include <KPIMTextEdit/PlainTextEditorWidget>
#include <MessageViewer/KXFace>
#include <KIO/StoredTransferJob>
#include <KJobWidgets>
#include <KLocalizedString>
#include <KMessageBox>
#include <QComboBox>
#include <QCheckBox>
#include <QFileDialog>
#include <QFontDatabase>
#include <QHBoxLayout>
#include <QImageReader>
#include <QLabel>
#include <QPushButton>
#include <QStackedWidget>
#include <QVBoxLayout>
using namespace KContacts;
using namespace KIO;
using namespace KMail;
using namespace MessageViewer;
#include <QBuffer>
#include <QProcess>
using namespace KMail;
using MessageViewer::KXFace;
// The size of the PNG used in the Face header must be at most 725 bytes, as
// explained here: https://quimby.gnus.org/circus/face/
#define FACE_MAX_SIZE 725
XFaceConfigurator::XFaceConfigurator(QWidget *parent)
: QWidget(parent)
, mEnableCheck(new QCheckBox(i18n("&Send picture with every message"), this))
, mXFaceLabel(new QLabel(this))
, mUi(new Ui::XFaceConfigurator)
, mPngquantProc(new QProcess(this))
{
auto vlay = new QVBoxLayout(this);
vlay->setObjectName(QStringLiteral("main layout"));
auto hlay = new QHBoxLayout();
vlay->addLayout(hlay);
// "enable X-Face" checkbox:
mEnableCheck->setWhatsThis(
i18n("Check this box if you want KMail to add a so-called X-Face header to messages "
"written with this identity. An X-Face is a small (48x48 pixels) black and "
"white image that some mail clients are able to display."));
hlay->addWidget(mEnableCheck, Qt::AlignLeft | Qt::AlignVCenter);
mXFaceLabel->setWhatsThis(i18n("This is a preview of the picture selected/entered below."));
mXFaceLabel->setFixedSize(48, 48);
mXFaceLabel->setFrameShape(QFrame::Box);
hlay->addWidget(mXFaceLabel);
// label1 = new QLabel( "X-Face:", this );
// vlay->addWidget( label1 );
// "obtain X-Face from" combo and label:
hlay = new QHBoxLayout(); // inherits spacing
vlay->addLayout(hlay);
auto sourceCombo = new QComboBox(this);
sourceCombo->setWhatsThis(i18n("Click on the widgets below to obtain help on the input methods."));
sourceCombo->setEnabled(false); // since !mEnableCheck->isChecked()
sourceCombo->addItems(QStringList() << i18nc("continuation of \"obtain picture from\"", "External Source")
<< i18nc("continuation of \"obtain picture from\"", "Input Field Below"));
auto label = new QLabel(i18n("Obtain pic&ture from:"), this);
label->setBuddy(sourceCombo);
label->setEnabled(false); // since !mEnableCheck->isChecked()
hlay->addWidget(label);
hlay->addWidget(sourceCombo, 1);
// widget stack that is controlled by the source combo:
auto widgetStack = new QStackedWidget(this);
widgetStack->setEnabled(false); // since !mEnableCheck->isChecked()
vlay->addWidget(widgetStack, 1);
connect(sourceCombo, &QComboBox::highlighted, widgetStack, &QStackedWidget::setCurrentIndex);
connect(sourceCombo, &QComboBox::activated, widgetStack, &QStackedWidget::setCurrentIndex);
connect(mEnableCheck, &QCheckBox::toggled, sourceCombo, &QComboBox::setEnabled);
connect(mEnableCheck, &QCheckBox::toggled, widgetStack, &QStackedWidget::setEnabled);
connect(mEnableCheck, &QCheckBox::toggled, label, &QLabel::setEnabled);
// The focus might be still in the widget that is disabled
connect(mEnableCheck, &QAbstractButton::clicked, mEnableCheck, qOverload<>(&QWidget::setFocus));
int pageno = 0;
// page 0: create X-Face from image file or address book entry
auto page = new QWidget(widgetStack);
widgetStack->insertWidget(pageno, page); // force sequential numbers (play safe)
auto page_vlay = new QVBoxLayout(page);
page_vlay->setContentsMargins({});
hlay = new QHBoxLayout(); // inherits spacing ??? FIXME really?
page_vlay->addLayout(hlay);
auto mFromFileBtn = new QPushButton(i18n("Select File..."), page);
mFromFileBtn->setWhatsThis(
i18n("Use this to select an image file to create the picture from. "
"The image should be of high contrast and nearly quadratic shape. "
"A light background helps improve the result."));
mFromFileBtn->setAutoDefault(false);
page_vlay->addWidget(mFromFileBtn, 1);
connect(mFromFileBtn, &QPushButton::released, this, &XFaceConfigurator::slotSelectFile);
auto mFromAddrbkBtn = new QPushButton(i18n("Set From Address Book"), page);
mFromAddrbkBtn->setWhatsThis(
i18n("You can use a scaled-down version of the picture "
"you have set in your address book entry."));
mFromAddrbkBtn->setAutoDefault(false);
page_vlay->addWidget(mFromAddrbkBtn, 1);
connect(mFromAddrbkBtn, &QPushButton::released, this, &XFaceConfigurator::slotSelectFromAddressbook);
auto label1 = new QLabel(i18n("<qt>KMail can send a small (48x48 pixels), low-quality, "
"monochrome picture with every message. "
"For example, this could be a picture of you or a glyph. "
"It is shown in the recipient's mail client (if supported).</qt>"),
page);
label1->setAlignment(Qt::AlignVCenter);
label1->setWordWrap(true);
page_vlay->addWidget(label1);
page_vlay->addStretch();
widgetStack->setCurrentIndex(0); // since sourceCombo->currentItem() == 0
// page 1: input field for direct entering
++pageno;
page = new QWidget(widgetStack);
widgetStack->insertWidget(pageno, page);
page_vlay = new QVBoxLayout(page);
page_vlay->setContentsMargins({});
mTextEdit = new KPIMTextEdit::PlainTextEditorWidget(page);
mTextEdit->editor()->setSpellCheckingSupport(false);
page_vlay->addWidget(mTextEdit);
mTextEdit->editor()->setWhatsThis(i18n("Use this field to enter an arbitrary X-Face string."));
mTextEdit->editor()->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont));
mTextEdit->editor()->setWordWrapMode(QTextOption::WrapAnywhere);
mTextEdit->editor()->setSearchSupport(false);
auto label2 = new QLabel(i18n("Examples are available at <a "
"href=\"https://ace.home.xs4all.nl/X-Faces/\">"
"https://ace.home.xs4all.nl/X-Faces/</a>."),
page);
label2->setOpenExternalLinks(true);
label2->setTextInteractionFlags(Qt::TextBrowserInteraction);
page_vlay->addWidget(label2);
connect(mTextEdit->editor(), &KPIMTextEdit::PlainTextEditor::textChanged, this, &XFaceConfigurator::slotUpdateXFace);
mUi->setupUi(this);
mPngquantProc->setInputChannelMode(QProcess::ManagedInputChannel);
mPngquantProc->setProgram(QLatin1String("pngquant"));
mPngquantProc->setArguments(QStringList() << QLatin1String("--strip") << QLatin1String("7") << QLatin1String("-"));
mUi->faceConfig->setTitle(i18n("Face"));
mUi->xFaceConfig->setTitle(i18n("X-Face"));
mUi->faceConfig->setInfo(i18n("More information under <a href=\"https://quimby.gnus.org/circus/face/\">https://quimby.gnus.org/circus/face/</a>."));
mUi->xFaceConfig->setInfo(i18n("Examples are available at <a href=\"https://ace.home.xs4all.nl/X-Faces/\">https://ace.home.xs4all.nl/X-Faces/</a>."));
connect(mUi->enableComboBox, &QComboBox::currentIndexChanged, this, &XFaceConfigurator::modeChanged);
connect(mUi->faceConfig, &EncodedImagePicker::imageSelected, this, &XFaceConfigurator::compressFace);
connect(mUi->xFaceConfig, &EncodedImagePicker::imageSelected, this, &XFaceConfigurator::compressXFace);
connect(mUi->faceConfig, &EncodedImagePicker::sourceChanged, this, &XFaceConfigurator::updateFace);
connect(mUi->xFaceConfig, &EncodedImagePicker::sourceChanged, this, &XFaceConfigurator::updateXFace);
connect(mPngquantProc, &QProcess::finished, this, &XFaceConfigurator::pngquantFinished);
// set initial state
modeChanged(mUi->enableComboBox->currentIndex());
}
XFaceConfigurator::~XFaceConfigurator() = default;
bool XFaceConfigurator::isXFaceEnabled() const
{
return mEnableCheck->isChecked();
return mUi->enableComboBox->currentIndex() & SendXFace;
}
void XFaceConfigurator::setXFaceEnabled(bool enable)
{
mEnableCheck->setChecked(enable);
const int currentIndex = mUi->enableComboBox->currentIndex();
if (enable) {
mUi->enableComboBox->setCurrentIndex(currentIndex | SendXFace);
} else {
mUi->enableComboBox->setCurrentIndex(currentIndex & ~SendXFace);
}
}
bool XFaceConfigurator::isFaceEnabled() const
{
return mUi->enableComboBox->currentIndex() & SendFace;
}
void XFaceConfigurator::setFaceEnabled(bool enable)
{
const int currentIndex = mUi->enableComboBox->currentIndex();
if (enable) {
mUi->enableComboBox->setCurrentIndex(currentIndex | SendFace);
} else {
mUi->enableComboBox->setCurrentIndex(currentIndex & ~SendFace);
}
}
QString XFaceConfigurator::xface() const
{
return mTextEdit->editor()->toPlainText();
QString str = mUi->xFaceConfig->source().trimmed();
str.remove(QStringLiteral("x-face:"), Qt::CaseInsensitive);
str = str.trimmed();
return str;
}
void XFaceConfigurator::setXFace(const QString &text)
{
mTextEdit->editor()->setPlainText(text);
mUi->xFaceConfig->setSource(text);
}
void XFaceConfigurator::setXfaceFromFile(const QUrl &url)
QString XFaceConfigurator::face() const
{
auto job = KIO::storedGet(url);
KJobWidgets::setWindow(job, this);
if (job->exec()) {
KXFace xf;
mTextEdit->editor()->setPlainText(xf.fromImage(QImage::fromData(job->data())));
} else {
KMessageBox::error(this, job->errorString());
}
QString str = mUi->faceConfig->source().trimmed();
str.remove(QStringLiteral("face:"), Qt::CaseInsensitive);
str = str.trimmed();
return str;
}
void XFaceConfigurator::slotSelectFile()
void XFaceConfigurator::setFace(const QString &text)
{
QString filter;
const QList<QByteArray> supportedImage = QImageReader::supportedImageFormats();
for (const QByteArray &ba : supportedImage) {
if (!filter.isEmpty()) {
filter += QLatin1Char(' ');
}
filter += QLatin1String("*.") + QString::fromLatin1(ba);
}
mUi->faceConfig->setSource(text);
}
filter = QStringLiteral("%1 (%2)").arg(i18n("Image"), filter);
const QUrl url = QFileDialog::getOpenFileUrl(this, QString(), QUrl(), filter);
if (!url.isEmpty()) {
setXfaceFromFile(url);
void XFaceConfigurator::modeChanged(int index)
{
mUi->faceConfig->setEnabled(index & SendFace);
mUi->xFaceConfig->setEnabled(index & SendXFace);
switch (index) {
case DontSend:
mUi->modeInfo->setText(i18n("No image will be sent."));
break;
case SendFace:
mUi->modeInfo->setText(i18n("KMail will send a colored image through the Face header."));
break;