diff --git a/CMakeLists.txt b/CMakeLists.txt index f56d31ef..ed01253d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,141 +1,141 @@ cmake_minimum_required(VERSION 3.5) set(PIM_VERSION "5.14.40") if (POLICY CMP0053) cmake_policy(SET CMP0053 NEW) endif() project(Messagelib VERSION ${PIM_VERSION}) option(MIMETREEPARSER_ONLY_BUILD "Build only mimetreeparser" FALSE) option(KDEPIM_ENTERPRISE_BUILD "Enable features specific to the enterprise branch, which are normally disabled. Also, it disables many components not needed for Kontact such as the Kolab client." FALSE) option(KDEPIM_RUN_AKONADI_TEST "Enable autotest based on Akonadi." TRUE) option(MESSAGEVIEWER_EXPERIMENTAL_CONVERSATIONVIEW "Experimental conversationview (in progress)" FALSE) set(KF5_MIN_VERSION "5.68.0") set(MESSAGELIB_LIB_VERSION ${PIM_VERSION}) set(AKONADIMIME_LIB_VERSION "5.14.40") set(QT_REQUIRED_VERSION "5.12.0") set(AKONADI_VERSION "5.14.40") set(GRANTLEETHEME_LIB_VERSION "5.14.40") set(GRAVATAR_LIB_VERSION "5.14.40") set(IDENTITYMANAGEMENT_LIB_VERSION "5.14.40") set(KDEPIM_APPS_LIB_VERSION "5.14.40") set(KLDAP_LIB_VERSION "5.14.40") set(KMAILTRANSPORT_LIB_VERSION "5.14.40") set(KMBOX_LIB_VERSION "5.14.40") set(KMIME_LIB_VERSION "5.14.40") -set(KPIMTEXTEDIT_LIB_VERSION "5.14.40") +set(KPIMTEXTEDIT_LIB_VERSION "5.14.41") set(LIBKDEPIM_LIB_VERSION "5.14.40") set(LIBKLEO_LIB_VERSION "5.14.40") set(PIMCOMMON_LIB_VERSION "5.14.40") set(GPGME_LIB_VERSION "1.11.1") set(AKONADI_CONTACT_VERSION "5.14.40") if (${MIMETREEPARSER_ONLY_BUILD}) set(ECM_VERSION "5.26.0") set(KMIME_LIB_VERSION "5.1.40") else() set(ECM_VERSION ${KF5_MIN_VERSION}) endif() find_package(ECM ${ECM_VERSION} CONFIG REQUIRED) set(CMAKE_MODULE_PATH ${Messagelib_SOURCE_DIR}/cmake/modules ${ECM_MODULE_PATH}) set(LIBRARY_NAMELINK) include(KDEInstallDirs) include(KDECMakeSettings) include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) include(GenerateExportHeader) include(ECMSetupVersion) include(ECMGenerateHeaders) include(ECMGeneratePriFile) include(FeatureSummary) include(ECMQtDeclareLoggingCategory) include(ECMAddTests) find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Gui Test) find_package(KF5Codecs ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5I18n ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Mime ${KMIME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5NewStuff ${KMIME_LIB_VERSION} CONFIG REQUIRED) find_package(QGpgme ${GPGME_LIB_VERSION} CONFIG REQUIRED) if (NOT ${MIMETREEPARSER_ONLY_BUILD}) find_package(Qt5 ${QT_REQUIRED_VERSION} CONFIG REQUIRED Widgets Network PrintSupport WebEngine WebEngineWidgets) find_package(KF5Archive ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Completion ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Config ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5ConfigWidgets ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5IconThemes ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5ItemViews ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5JobWidgets ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5KIO ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Service ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5Sonnet ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5TextWidgets ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5WidgetsAddons ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5XmlGui ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5SyntaxHighlighting ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5DBusAddons ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(Grantlee5 "5.2" CONFIG REQUIRED) #Use KF5_VERSION in the future find_package(KF5Akonadi ${AKONADI_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiMime ${AKONADIMIME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Contacts ${KF5_MIN_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiContact ${AKONADI_CONTACT_VERSION} CONFIG REQUIRED) find_package(KF5FollowupReminder ${KDEPIM_APPS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5GrantleeTheme ${GRANTLEETHEME_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Gravatar ${GRAVATAR_LIB_VERSION} CONFIG REQUIRED) find_package(KF5IdentityManagement ${IDENTITYMANAGEMENT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5KaddressbookGrantlee ${KDEPIM_APPS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Ldap ${KLDAP_LIB_VERSION} CONFIG REQUIRED) find_package(KF5LibkdepimAkonadi ${LIBKDEPIM_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Libkleo ${LIBKLEO_LIB_VERSION} CONFIG REQUIRED) find_package(KF5MailTransportAkonadi ${KMAILTRANSPORT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5Mbox ${KMBOX_LIB_VERSION} CONFIG REQUIRED) find_package(KF5PimCommonAkonadi ${PIMCOMMON_LIB_VERSION} CONFIG REQUIRED) find_package(KF5PimTextEdit ${KPIMTEXTEDIT_LIB_VERSION} CONFIG REQUIRED) find_package(KF5SendLater ${KDEPIM_APPS_LIB_VERSION} CONFIG REQUIRED) find_package(KF5AkonadiSearch "5.14.40" CONFIG REQUIRED) set_package_properties(KF5AkonadiSearch PROPERTIES DESCRIPTION "The Akonadi Search libraries" URL "https://kde.org/" TYPE REQUIRED PURPOSE "Provides search capabilities in KMail and Akonadi") endif() set(CMAKE_CXX_STANDARD 14) if (EXISTS "${CMAKE_SOURCE_DIR}/.git") add_definitions(-DQT_DISABLE_DEPRECATED_BEFORE=0x050e00) add_definitions(-DKF_DISABLE_DEPRECATED_BEFORE_AND_AT=0x054400) endif() if(BUILD_TESTING) add_definitions(-DBUILD_TESTING) endif() add_subdirectory(mimetreeparser) if (NOT ${MIMETREEPARSER_ONLY_BUILD}) add_subdirectory(messageviewer) add_subdirectory(templateparser) add_subdirectory(messagecomposer) add_subdirectory(messagecore) add_subdirectory(messagelist) add_subdirectory(webengineviewer) endif() ecm_qt_install_logging_categories( EXPORT MESSAGELIB FILE messagelib.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} ) feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/messagecomposer/src/composer-ng/richtextcomposerng.cpp b/messagecomposer/src/composer-ng/richtextcomposerng.cpp index 40207b08..c654e370 100644 --- a/messagecomposer/src/composer-ng/richtextcomposerng.cpp +++ b/messagecomposer/src/composer-ng/richtextcomposerng.cpp @@ -1,418 +1,435 @@ /* Copyright (C) 2015-2020 Laurent Montel 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) any later version. 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; see the file COPYING. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "richtextcomposerng.h" #include #include #include "richtextcomposersignatures.h" #include #include #include "settings/messagecomposersettings.h" #include #include +#include using namespace MessageComposer; class MessageComposer::RichTextComposerNgPrivate { public: RichTextComposerNgPrivate(RichTextComposerNg *q) : richtextComposer(q) { richTextComposerSignatures = new MessageComposer::RichTextComposerSignatures(richtextComposer, richtextComposer); } void fixHtmlFontSize(QString &cleanHtml); QString toCleanHtml() const; PimCommon::AutoCorrection *autoCorrection = nullptr; RichTextComposerNg *richtextComposer = nullptr; MessageComposer::RichTextComposerSignatures *richTextComposerSignatures = nullptr; }; RichTextComposerNg::RichTextComposerNg(QWidget *parent) : KPIMTextEdit::RichTextComposer(parent) , d(new MessageComposer::RichTextComposerNgPrivate(this)) { } RichTextComposerNg::~RichTextComposerNg() { delete d; } MessageComposer::RichTextComposerSignatures *RichTextComposerNg::composerSignature() const { return d->richTextComposerSignatures; } PimCommon::AutoCorrection *RichTextComposerNg::autocorrection() const { return d->autoCorrection; } void RichTextComposerNg::setAutocorrection(PimCommon::AutoCorrection *autocorrect) { d->autoCorrection = autocorrect; } void RichTextComposerNg::setAutocorrectionLanguage(const QString &lang) { if (d->autoCorrection) { d->autoCorrection->setLanguage(lang); } } static bool isSpecial(const QTextCharFormat &charFormat) { return charFormat.isFrameFormat() || charFormat.isImageFormat() || charFormat.isListFormat() || charFormat.isTableFormat() || charFormat.isTableCellFormat(); } bool RichTextComposerNg::processModifyText(QKeyEvent *e) { if (d->autoCorrection && d->autoCorrection->isEnabledAutoCorrection()) { if ((e->key() == Qt::Key_Space) || (e->key() == Qt::Key_Enter) || (e->key() == Qt::Key_Return)) { if (!isLineQuoted(textCursor().block().text()) && !textCursor().hasSelection()) { const QTextCharFormat initialTextFormat = textCursor().charFormat(); const bool richText = (textMode() == RichTextComposer::Rich); int position = textCursor().position(); const bool addSpace = d->autoCorrection->autocorrect(richText, *document(), position); QTextCursor cur = textCursor(); cur.setPosition(position); const bool spacePressed = (e->key() == Qt::Key_Space); if (overwriteMode() && spacePressed) { if (addSpace) { const QChar insertChar = QLatin1Char(' '); if (!cur.atBlockEnd()) { cur.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, 1); } if (richText && !isSpecial(initialTextFormat)) { cur.insertText(insertChar, initialTextFormat); } else { cur.insertText(insertChar); } setTextCursor(cur); } } else { const QChar insertChar = spacePressed ? QLatin1Char(' ') : QLatin1Char('\n'); if (richText && !isSpecial(initialTextFormat)) { if ((spacePressed && addSpace) || !spacePressed) { cur.insertText(insertChar, initialTextFormat); } } else { if ((spacePressed && addSpace) || !spacePressed) { cur.insertText(insertChar); } } setTextCursor(cur); } return true; } } } return false; } void RichTextComposerNgPrivate::fixHtmlFontSize(QString &cleanHtml) { static const QString FONTSTYLEREGEX = QStringLiteral(""); QRegExp styleRegex(FONTSTYLEREGEX); styleRegex.setMinimal(true); int offset = styleRegex.indexIn(cleanHtml, 0); while (offset != -1) { // replace all the matching text with the new line text bool ok = false; const QString fontSizeStr = styleRegex.cap(1); const int ptValue = fontSizeStr.toInt(&ok); if (ok) { double emValue = static_cast(ptValue) / 12; const QString emValueStr = QString::number(emValue, 'g', 2); cleanHtml.replace(styleRegex.pos(1), QString(fontSizeStr + QLatin1String("px")).length(), emValueStr + QLatin1String("em")); } // advance the search offset to just beyond the last replace offset += styleRegex.matchedLength(); // find the next occurrence offset = styleRegex.indexIn(cleanHtml, offset); } } MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus RichTextComposerNg::convertPlainText(MessageComposer::TextPart *textPart) { Q_UNUSED(textPart); return MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::NotConverted; } +#define USE_TEXTHTML_BUILDER 1 + void RichTextComposerNg::fillComposerTextPart(MessageComposer::TextPart *textPart) { bool wasConverted = convertPlainText(textPart) == MessageComposer::PluginEditorConvertTextInterface::ConvertTextStatus::Converted; if (composerControler()->isFormattingUsed()) { if (!wasConverted) { if (MessageComposer::MessageComposerSettings::self()->improvePlainTextOfHtmlMessage()) { Grantlee::PlainTextMarkupBuilder *pb = new Grantlee::PlainTextMarkupBuilder(); Grantlee::MarkupDirector *pmd = new Grantlee::MarkupDirector(pb); pmd->processDocument(document()); const QString plainText = pb->getResult(); textPart->setCleanPlainText(composerControler()->toCleanPlainText(plainText)); QTextDocument *doc = new QTextDocument(plainText); doc->adjustSize(); textPart->setWrappedPlainText(composerControler()->toWrappedPlainText(doc)); delete doc; delete pmd; delete pb; } else { textPart->setCleanPlainText(composerControler()->toCleanPlainText()); textPart->setWrappedPlainText(composerControler()->toWrappedPlainText()); } } } else { if (!wasConverted) { textPart->setCleanPlainText(composerControler()->toCleanPlainText()); textPart->setWrappedPlainText(composerControler()->toWrappedPlainText()); } } textPart->setWordWrappingEnabled(lineWrapMode() == QTextEdit::FixedColumnWidth); if (composerControler()->isFormattingUsed() && !wasConverted) { +#ifdef USE_TEXTHTML_BUILDER + KPIMTextEdit::TextHTMLBuilder *pb = new KPIMTextEdit::TextHTMLBuilder(); + + Grantlee::MarkupDirector *pmd = new Grantlee::MarkupDirector(pb); + pmd->processDocument(document()); + QString cleanHtml = QStringLiteral("\n\n\n\n%1\n").arg(pb->getResult()); + delete pmd; + delete pb; + d->fixHtmlFontSize(cleanHtml); + textPart->setCleanHtml(cleanHtml); +#else QString cleanHtml = d->toCleanHtml(); d->fixHtmlFontSize(cleanHtml); textPart->setCleanHtml(cleanHtml); +#endif textPart->setEmbeddedImages(composerControler()->composerImages()->embeddedImages()); } } QString RichTextComposerNgPrivate::toCleanHtml() const { + QString result = richtextComposer->toHtml(); + static const QString EMPTYLINEHTML = QStringLiteral( "

 

"); // Qt inserts various style properties based on the current mode of the editor (underline, // bold, etc), but only empty paragraphs *also* have qt-paragraph-type set to 'empty'. static const QString EMPTYLINEREGEX = QStringLiteral( "

"); static const QString OLLISTPATTERNQT = QStringLiteral( "

    elements with

     

    . QRegExp emptyLineFinder(EMPTYLINEREGEX); emptyLineFinder.setMinimal(true); // find the first occurrence int offset = emptyLineFinder.indexIn(result, 0); while (offset != -1) { // replace all the matching text with the new line text result.replace(offset, emptyLineFinder.matchedLength(), EMPTYLINEHTML); // advance the search offset to just beyond the last replace offset += EMPTYLINEHTML.length(); // find the next occurrence offset = emptyLineFinder.indexIn(result, offset); } // fix 2a - ordered lists - MS Outlook treats margin-left:0px; as // a non-existing number; e.g: "1. First item" turns into "First Item" result.replace(OLLISTPATTERNQT, ORDEREDLISTHTML); // fix 2b - unordered lists - MS Outlook treats margin-left:0px; as // a non-existing bullet; e.g: "* First bullet" turns into "First Bullet" result.replace(ULLISTPATTERNQT, UNORDEREDLISTHTML); return result; } static bool isCursorAtEndOfLine(const QTextCursor &cursor) { QTextCursor testCursor = cursor; testCursor.movePosition(QTextCursor::EndOfLine, QTextCursor::KeepAnchor); return !testCursor.hasSelection(); } static void insertSignatureHelper(const QString &signature, RichTextComposerNg *textEdit, KIdentityManagement::Signature::Placement placement, bool isHtml, bool addNewlines) { if (!signature.isEmpty()) { // Save the modified state of the document, as inserting a signature // shouldn't change this. Restore it at the end of this function. bool isModified = textEdit->document()->isModified(); // Move to the desired position, where the signature should be inserted QTextCursor cursor = textEdit->textCursor(); QTextCursor oldCursor = cursor; cursor.beginEditBlock(); if (placement == KIdentityManagement::Signature::End) { cursor.movePosition(QTextCursor::End); } else if (placement == KIdentityManagement::Signature::Start) { cursor.movePosition(QTextCursor::Start); } else if (placement == KIdentityManagement::Signature::AtCursor) { cursor.movePosition(QTextCursor::StartOfLine); } textEdit->setTextCursor(cursor); QString lineSep; if (addNewlines) { if (isHtml) { lineSep = QStringLiteral("
    "); } else { lineSep = QLatin1Char('\n'); } } // Insert the signature and newlines depending on where it was inserted. int newCursorPos = -1; QString headSep; QString tailSep; if (placement == KIdentityManagement::Signature::End) { // There is one special case when re-setting the old cursor: The cursor // was at the end. In this case, QTextEdit has no way to know // if the signature was added before or after the cursor, and just // decides that it was added before (and the cursor moves to the end, // but it should not when appending a signature). See bug 167961 if (oldCursor.position() == textEdit->toPlainText().length()) { newCursorPos = oldCursor.position(); } headSep = lineSep; } else if (placement == KIdentityManagement::Signature::Start) { // When prepending signatures, add a couple of new lines before // the signature, and move the cursor to the beginning of the QTextEdit. // People tends to insert new text there. newCursorPos = 0; headSep = lineSep + lineSep; if (!isCursorAtEndOfLine(cursor)) { tailSep = lineSep; } } else if (placement == KIdentityManagement::Signature::AtCursor) { if (!isCursorAtEndOfLine(cursor)) { tailSep = lineSep; } } const QString full_signature = headSep + signature + tailSep; if (isHtml) { textEdit->insertHtml(full_signature); } else { textEdit->insertPlainText(full_signature); } cursor.endEditBlock(); if (newCursorPos != -1) { oldCursor.setPosition(newCursorPos); } textEdit->setTextCursor(oldCursor); textEdit->ensureCursorVisible(); textEdit->document()->setModified(isModified); if (isHtml) { textEdit->activateRichText(); } } } void RichTextComposerNg::insertSignature(const KIdentityManagement::Signature &signature, KIdentityManagement::Signature::Placement placement, KIdentityManagement::Signature::AddedText addedText) { if (signature.isEnabledSignature()) { QString signatureStr; if (addedText & KIdentityManagement::Signature::AddSeparator) { signatureStr = signature.withSeparator(); } else { signatureStr = signature.rawText(); } insertSignatureHelper(signatureStr, this, placement, (signature.isInlinedHtml() && signature.type() == KIdentityManagement::Signature::Inlined), (addedText & KIdentityManagement::Signature::AddNewLines)); // We added the text of the signature above, now it is time to add the images as well. if (signature.isInlinedHtml()) { for (const KIdentityManagement::Signature::EmbeddedImagePtr &image : signature.embeddedImages()) { composerControler()->composerImages()->loadImage(image->image, image->name, image->name); } } } } QString RichTextComposerNg::toCleanHtml() const { return d->toCleanHtml(); } void RichTextComposerNg::forceAutoCorrection(bool selectedText) { if (document()->isEmpty()) { return; } if (d->autoCorrection && d->autoCorrection->isEnabledAutoCorrection()) { const bool richText = (textMode() == RichTextComposer::Rich); const int initialPosition = textCursor().position(); QTextCursor cur = textCursor(); cur.beginEditBlock(); if (selectedText && cur.hasSelection()) { const int positionStart = qMin(cur.selectionEnd(), cur.selectionStart()); const int positionEnd = qMax(cur.selectionEnd(), cur.selectionStart()); cur.setPosition(positionStart); int cursorPosition = positionStart; while (cursorPosition < positionEnd) { if (isLineQuoted(cur.block().text())) { cur.movePosition(QTextCursor::NextBlock); } else { cur.movePosition(QTextCursor::NextWord); } cursorPosition = cur.position(); (void)d->autoCorrection->autocorrect(richText, *document(), cursorPosition); } } else { cur.movePosition(QTextCursor::Start); while (!cur.atEnd()) { if (isLineQuoted(cur.block().text())) { cur.movePosition(QTextCursor::NextBlock); } else { cur.movePosition(QTextCursor::NextWord); } int cursorPosition = cur.position(); (void)d->autoCorrection->autocorrect(richText, *document(), cursorPosition); } } cur.endEditBlock(); if (cur.position() != initialPosition) { cur.setPosition(initialPosition); setTextCursor(cur); } } }