diff --git a/autotests/slidecontainertest.cpp b/autotests/slidecontainertest.cpp index 845902a..8163965 100644 --- a/autotests/slidecontainertest.cpp +++ b/autotests/slidecontainertest.cpp @@ -1,140 +1,140 @@ /* Gwenview: an image viewer Copyright 2011 Aurélien Gâteau 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; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Cambridge, MA 02110-1301, USA. */ // Self #include "slidecontainertest.h" // Local #include "kpimtextedit/slidecontainer.h" #include #include #include #include using namespace KPIMTextEdit; struct TestWindow : public QWidget { explicit TestWindow(QWidget *parent = nullptr) : QWidget(parent) , mContainer(new SlideContainer) , mContent(nullptr) { createContent(); mMainWidget = new QTextEdit(); QVBoxLayout *layout = new QVBoxLayout(this); layout->setSpacing(0); layout->setContentsMargins(0, 0, 0, 0); layout->addWidget(mMainWidget); layout->addWidget(mContainer); } void createContent() { mContent = new QTextEdit; mContent->setFixedSize(100, 40); mContainer->setContent(mContent); } SlideContainer *mContainer; QWidget *mMainWidget; QWidget *mContent; }; void SlideContainerAutoTest::testInit() { // Even with content, a SlideContainer should be invisible until slideIn() // is called TestWindow window; window.show(); QTest::qWait(500); QCOMPARE(window.mMainWidget->height(), window.height()); } void SlideContainerAutoTest::testSlideIn() { TestWindow window; - QSignalSpy inSpy(window.mContainer, SIGNAL(slidedIn())); - QSignalSpy outSpy(window.mContainer, SIGNAL(slidedOut())); + QSignalSpy inSpy(window.mContainer, &SlideContainer::slidedIn); + QSignalSpy outSpy(window.mContainer, &SlideContainer::slidedOut); window.show(); window.mContainer->slideIn(); while (window.mContainer->slideHeight() != window.mContent->height()) { QTest::qWait(100); } QCOMPARE(window.mContainer->height(), window.mContent->height()); QCOMPARE(inSpy.count(), 1); QCOMPARE(outSpy.count(), 0); } void SlideContainerAutoTest::testSlideOut() { TestWindow window; window.show(); window.mContainer->slideIn(); while (window.mContainer->slideHeight() != window.mContent->height()) { QTest::qWait(100); } - QSignalSpy inSpy(window.mContainer, SIGNAL(slidedIn())); - QSignalSpy outSpy(window.mContainer, SIGNAL(slidedOut())); + QSignalSpy inSpy(window.mContainer, &SlideContainer::slidedIn); + QSignalSpy outSpy(window.mContainer, &SlideContainer::slidedOut); window.mContainer->slideOut(); while (window.mContainer->slideHeight() != 0) { QTest::qWait(100); } QCOMPARE(window.mContainer->height(), 0); QCOMPARE(inSpy.count(), 0); QCOMPARE(outSpy.count(), 1); } void SlideContainerAutoTest::testSlideInDeleteSlideOut() { // If content is deleted while visible, slideOut() should still produce an // animation TestWindow window; window.show(); window.mContainer->slideIn(); while (window.mContainer->slideHeight() != window.mContent->height()) { QTest::qWait(100); } window.mContent->deleteLater(); window.mContainer->slideOut(); while (window.mContainer->slideHeight() != 0) { QTest::qWait(100); } QCOMPARE(window.mContainer->height(), 0); } void SlideContainerAutoTest::testHiddenContentResize() { // Resizing content should not trigger a slide if it is not visible. TestWindow window; window.show(); QVERIFY(QTest::qWaitForWindowExposed(&window)); window.mContent->show(); window.mContent->setFixedSize(150, 80); QTest::qWait(500); QCOMPARE(window.mContainer->height(), 0); } QTEST_MAIN(SlideContainerAutoTest) diff --git a/src/composer-ng/autotests/richtextcomposertest.cpp b/src/composer-ng/autotests/richtextcomposertest.cpp index d96eec6..b2464bc 100644 --- a/src/composer-ng/autotests/richtextcomposertest.cpp +++ b/src/composer-ng/autotests/richtextcomposertest.cpp @@ -1,540 +1,540 @@ /* Copyright (C) 2015-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "richtextcomposertest.h" #include "../richtextcomposer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace KPIMTextEdit; Q_DECLARE_METATYPE(KPIMTextEdit::RichTextComposer::Mode) RichTextComposerTest::RichTextComposerTest(QObject *parent) : QObject(parent) { qRegisterMetaType(); QIcon::setThemeName(QStringLiteral("breeze")); QStandardPaths::setTestModeEnabled(true); } RichTextComposerTest::~RichTextComposerTest() { } void RichTextComposerTest::testFormattingUsed() { // This method tries to test everything that krichtextedit makes available, so // we can sure that in KMail, when the user uses some formatting, the mail is actually // sent as HTML mail KPIMTextEdit::RichTextComposer textEdit; textEdit.createActions(new KActionCollection(this)); QVERIFY(!textEdit.composerControler()->isFormattingUsed()); // Insert some text. QTextCursor cursor(textEdit.document()); cursor.insertText(QStringLiteral("Hello World!!")); QVERIFY(!textEdit.composerControler()->isFormattingUsed()); cursor.setPosition(1); textEdit.setTextCursor(cursor); // // Test link // QString someUrl = QStringLiteral("www.test.de"); QString altText = QStringLiteral("Hello"); textEdit.composerControler()->updateLink(someUrl, altText); QVERIFY(textEdit.composerControler()->isFormattingUsed()); QCOMPARE(textEdit.composerControler()->currentLinkUrl(), someUrl); QCOMPARE(textEdit.composerControler()->currentLinkText(), altText); cursor.setPosition(1); textEdit.setTextCursor(cursor); textEdit.composerControler()->updateLink(QString(), QStringLiteral("Hello")); QVERIFY(textEdit.composerControler()->currentLinkUrl().isEmpty()); QVERIFY(!textEdit.composerControler()->currentLinkText().isEmpty()); QVERIFY(!textEdit.composerControler()->isFormattingUsed()); // // Test alignment // cursor.setPosition(1); textEdit.setTextCursor(cursor); textEdit.composerControler()->alignRight(); QVERIFY(textEdit.composerControler()->isFormattingUsed()); QCOMPARE(textEdit.alignment(), Qt::AlignRight); textEdit.composerControler()->alignLeft(); QVERIFY(!textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->alignCenter(); QCOMPARE(textEdit.alignment(), Qt::AlignHCenter); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->alignJustify(); QCOMPARE(textEdit.alignment(), Qt::AlignJustify); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->alignLeft(); QCOMPARE(textEdit.alignment(), Qt::AlignLeft); QVERIFY(!textEdit.composerControler()->isFormattingUsed()); // // Test layout direction // textEdit.selectAll(); QTextCharFormat direction; direction.setLayoutDirection(Qt::RightToLeft); textEdit.mergeCurrentCharFormat(direction); QVERIFY(textEdit.composerControler()->isFormattingUsed()); direction.setLayoutDirection(Qt::LeftToRight); textEdit.mergeCurrentCharFormat(direction); QVERIFY(textEdit.composerControler()->isFormattingUsed()); // // Test lists // textEdit.composerControler()->setListStyle(QTextListFormat::ListCircle); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setListStyle(0); QVERIFY(textEdit.composerControler()->isFormattingUsed()); // // Test font attributes // textEdit.setFontFamily(QStringLiteral("Times")); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.setFontFamily(textEdit.document()->defaultFont().family()); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setFontSize(48); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setFontSize(textEdit.document()->defaultFont().pointSize()); QVERIFY(textEdit.composerControler()->isFormattingUsed()); QFont myFont = textEdit.document()->defaultFont(); myFont.setStyle(QFont::StyleOblique); textEdit.composerControler()->setFont(myFont); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setFont(textEdit.document()->defaultFont()); QVERIFY(textEdit.composerControler()->isFormattingUsed()); // // Test bold, italic, underline and strikeout // textEdit.composerControler()->setTextBold(true); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextBold(false); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextUnderline(true); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextUnderline(false); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextItalic(true); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextItalic(false); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextStrikeOut(true); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextStrikeOut(false); QVERIFY(textEdit.composerControler()->isFormattingUsed()); // // Color // QColor oldForeground = textEdit.document()->firstBlock().charFormat().foreground().color(); textEdit.composerControler()->setTextForegroundColor(Qt::red); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextForegroundColor(oldForeground); QVERIFY(textEdit.composerControler()->isFormattingUsed()); QColor oldBackground = textEdit.document()->firstBlock().charFormat().background().color(); textEdit.composerControler()->setTextBackgroundColor(Qt::red); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextBackgroundColor(oldBackground); QVERIFY(textEdit.composerControler()->isFormattingUsed()); // // Horizontal rule // textEdit.composerControler()->insertHorizontalRule(); QVERIFY(textEdit.composerControler()->isFormattingUsed()); // No way to easily remove the horizontal line, so clear the text edit and start over textEdit.clear(); cursor.insertText(QStringLiteral("Hello World!!")); QVERIFY(!textEdit.composerControler()->isFormattingUsed()); cursor.setPosition(1); textEdit.setTextCursor(cursor); // // Sub and superscript // textEdit.composerControler()->setTextSuperScript(true); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextSuperScript(false); QVERIFY(!textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextSubScript(true); QVERIFY(textEdit.composerControler()->isFormattingUsed()); textEdit.composerControler()->setTextSubScript(false); QVERIFY(!textEdit.composerControler()->isFormattingUsed()); // // Image // const QString imagePath = KIconLoader::global()->iconPath(QStringLiteral("folder-new"), KIconLoader::Small, false); textEdit.composerControler()->composerImages()->addImage(QUrl::fromLocalFile(imagePath)); QVERIFY(textEdit.composerControler()->isFormattingUsed()); cursor = textEdit.textCursor(); cursor.movePosition(QTextCursor::Left, QTextCursor::KeepAnchor, 1); cursor.removeSelectedText(); QVERIFY(!textEdit.composerControler()->isFormattingUsed()); } void RichTextComposerTest::testQuoting() { KPIMTextEdit::RichTextComposer edit; edit.createActions(new KActionCollection(this)); QVERIFY(edit.isLineQuoted(QStringLiteral("> Hello"))); QVERIFY(edit.isLineQuoted(QStringLiteral(">Hello"))); QVERIFY(!edit.isLineQuoted(QStringLiteral("Hello"))); QCOMPARE(edit.quoteLength(QStringLiteral("Hello")), 0); QCOMPARE(edit.quoteLength(QStringLiteral(">Hello")), 1); QCOMPARE(edit.quoteLength(QStringLiteral("> Hello")), 2); QCOMPARE(edit.quoteLength(QStringLiteral(">>>Hello")), 3); QCOMPARE(edit.quoteLength(QStringLiteral("> > > Hello")), 6); QCOMPARE(edit.quoteLength(QStringLiteral("|Hello")), 1); QCOMPARE(edit.quoteLength(QStringLiteral("| |Hello")), 3); } void RichTextComposerTest::testCleanText() { KPIMTextEdit::RichTextComposer edit; edit.createActions(new KActionCollection(this)); QString html(QStringLiteral("Heelllo World
Bye!")); QString plain(QStringLiteral("Heelllo World\nBye!")); edit.setTextOrHtml(html); edit.composerControler()->composerImages()->addImage(QUrl::fromLocalFile(KIconLoader::global()->iconPath(QStringLiteral("folder-new"), KIconLoader::Small, false))); QVERIFY(edit.textMode() == KPIMTextEdit::RichTextComposer::Rich); QCOMPARE(edit.composerControler()->toCleanPlainText(), plain); edit.show(); // < otherwise toWrappedPlainText can't work, it needs a layout QCOMPARE(edit.composerControler()->toWrappedPlainText(), plain); } void RichTextComposerTest::testEnter_data() { QTest::addColumn("initalText"); QTest::addColumn("expectedText"); QTest::addColumn("cursorPos"); QTest::newRow("") << QStringLiteral("> Hello World") << QStringLiteral("> Hello \n> World") << 8; QTest::newRow("") << QStringLiteral("Hello World") << QStringLiteral("Hello \nWorld") << 6; QTest::newRow("") << QStringLiteral("> Hello World") << QStringLiteral("> Hello World\n") << 13; QTest::newRow("") << QStringLiteral(">Hello World") << QStringLiteral(">Hello \n>World") << 7; QTest::newRow("") << QStringLiteral("> > Hello World") << QStringLiteral("> > Hello \n> > World") << 10; QTest::newRow("") << QStringLiteral("| | Hello World") << QStringLiteral("| | Hello \n| | World") << 10; } void RichTextComposerTest::testEnter() { QFETCH(QString, initalText); QFETCH(QString, expectedText); QFETCH(int, cursorPos); KPIMTextEdit::RichTextComposer edit; edit.createActions(new KActionCollection(this)); edit.setPlainText(initalText); QTextCursor textCursor(edit.document()); textCursor.setPosition(cursorPos); edit.setTextCursor(textCursor); QTest::keyClick(&edit, Qt::Key_Return); QCOMPARE(edit.toPlainText(), expectedText); } void RichTextComposerTest::testImages() { KPIMTextEdit::RichTextComposer edit; edit.createActions(new KActionCollection(this)); QString image1Path = KIconLoader::global()->iconPath(QStringLiteral("folder-new"), KIconLoader::Small, false); QString image2Path = KIconLoader::global()->iconPath(QStringLiteral("arrow-up"), KIconLoader::Small, false); // Add one image, check that embeddedImages() returns the right stuff edit.composerControler()->composerImages()->addImage(QUrl::fromLocalFile(image1Path)); KPIMTextEdit::ImageList images = edit.composerControler()->composerImages()->embeddedImages(); KPIMTextEdit::ImageWithNameList imagesWithNames = edit.composerControler()->composerImages()->imagesWithName(); QCOMPARE(images.size(), 1); QCOMPARE(imagesWithNames.size(), 1); EmbeddedImage *image = images.first().data(); ImageWithName *imageWithName = imagesWithNames.first().data(); QCOMPARE(image->imageName, QString::fromLatin1("folder-new.png")); QCOMPARE(imageWithName->name, QString::fromLatin1("folder-new.png")); // Also check that it loads the correct image QImage diskImage(image1Path); QBuffer buffer; buffer.open(QIODevice::WriteOnly); diskImage.save(&buffer, "PNG"); QBuffer imageWithNameBuffer; imageWithNameBuffer.open(QIODevice::WriteOnly); imageWithName->image.save(&imageWithNameBuffer, "PNG"); QByteArray encodedImage = KCodecs::Codec::codecForName("base64")->encode(buffer.buffer()); QCOMPARE(image->image, encodedImage); QCOMPARE(buffer.buffer(), imageWithNameBuffer.buffer()); // No image should be there after clearing edit.clear(); QVERIFY(edit.composerControler()->composerImages()->embeddedImages().isEmpty()); QVERIFY(edit.composerControler()->composerImages()->imagesWithName().isEmpty()); // Check that manually removing the image also empties the image list edit.composerControler()->composerImages()->addImage(QUrl::fromLocalFile(image1Path)); QCOMPARE(edit.composerControler()->composerImages()->embeddedImages().size(), 1); QCOMPARE(edit.composerControler()->composerImages()->imagesWithName().size(), 1); QTextCursor cursor = edit.textCursor(); cursor.setPosition(0, QTextCursor::MoveAnchor); cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor, 1); cursor.removeSelectedText(); QVERIFY(edit.composerControler()->composerImages()->embeddedImages().isEmpty()); QVERIFY(edit.composerControler()->composerImages()->imagesWithName().isEmpty()); // Check that adding the identical image two times only adds the image once edit.composerControler()->composerImages()->addImage(QUrl::fromLocalFile(image1Path)); edit.composerControler()->composerImages()->addImage(QUrl::fromLocalFile(image1Path)); QCOMPARE(edit.composerControler()->composerImages()->embeddedImages().size(), 1); QCOMPARE(edit.composerControler()->composerImages()->imagesWithName().size(), 1); // Another different image added, and we should have two images edit.clear(); edit.composerControler()->composerImages()->addImage(QUrl::fromLocalFile(image1Path)); edit.composerControler()->composerImages()->addImage(QUrl::fromLocalFile(image2Path)); images = edit.composerControler()->composerImages()->embeddedImages(); imagesWithNames = edit.composerControler()->composerImages()->imagesWithName(); QCOMPARE(images.size(), 2); QCOMPARE(imagesWithNames.size(), 2); KPIMTextEdit::EmbeddedImage *image1 = images.first().data(); KPIMTextEdit::EmbeddedImage *image2 = images.last().data(); KPIMTextEdit::ImageWithName *imageWithName1 = imagesWithNames.first().data(); KPIMTextEdit::ImageWithName *imageWithName2 = imagesWithNames.last().data(); QCOMPARE(image1->imageName, QString::fromLatin1("folder-new2.png")); // ### FIXME: should be folder-new.png, but QTextEdit provides no way to remove cached resources! QCOMPARE(imageWithName1->name, QString::fromLatin1("folder-new2.png")); QCOMPARE(image2->imageName, QString::fromLatin1("arrow-up.png")); QCOMPARE(imageWithName2->name, QString::fromLatin1("arrow-up.png")); QVERIFY(image1->contentID != image2->contentID); } void RichTextComposerTest::testImageHtmlCode() { KPIMTextEdit::RichTextComposer edit; edit.createActions(new KActionCollection(this)); QString image1Path = KIconLoader::global()->iconPath(QStringLiteral("folder-new"), KIconLoader::Small, false); QString image2Path = KIconLoader::global()->iconPath(QStringLiteral("arrow-up"), KIconLoader::Small, false); edit.composerControler()->composerImages()->addImage(QUrl::fromLocalFile(image1Path)); edit.composerControler()->composerImages()->addImage(QUrl::fromLocalFile(image2Path)); KPIMTextEdit::ImageList images = edit.composerControler()->composerImages()->embeddedImages(); QCOMPARE(images.size(), 2); KPIMTextEdit::EmbeddedImage *image1 = images.first().data(); KPIMTextEdit::EmbeddedImage *image2 = images.last().data(); QString startHtml = QStringLiteral("BlaBlub"); QString endHtml = QStringLiteral("BlaBlub") .arg(image2->contentID).arg(image1->contentID); QCOMPARE(KPIMTextEdit::RichTextComposerImages::imageNamesToContentIds(startHtml.toLatin1(), images), endHtml.toLatin1()); } void RichTextComposerTest::testDeleteLine_data() { QTest::addColumn("initalText"); QTest::addColumn("expectedText"); QTest::addColumn("cursorPos"); QTest::newRow("delete1") << QStringLiteral("line1\nline2\nline3") << QStringLiteral("line1\nline3") << 6; QTest::newRow("delete2") << QStringLiteral("line1\nline2\nline3") << QStringLiteral("line2\nline3") << 5; QTest::newRow("delete3") << QStringLiteral("line1\nline2\nline3") << QStringLiteral("line1\nline3") << 11; QTest::newRow("delete4") << QStringLiteral("line1\nline2\nline3") << QStringLiteral("line2\nline3") << 0; QTest::newRow("delete5") << QStringLiteral("line1\nline2\nline3") << QStringLiteral("line1\nline2") << 17; QTest::newRow("delete6") << QStringLiteral("line1") << QString() << 0; QTest::newRow("delete7") << QStringLiteral("line1") << QString() << 5; // Now, test deletion with word wrapping. The line with the Ms is so long that it will get wrapped QTest::newRow("delete8") << QStringLiteral("line1\nMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3") << QStringLiteral("line1\nMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3") << 6; QTest::newRow("delete9") << QStringLiteral("line1\nMMMMMMM MMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3") << QStringLiteral("line1\nMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM\nline3") << 13; } void RichTextComposerTest::testDeleteLine() { #if 0 QFETCH(QString, initalText); QFETCH(QString, expectedText); QFETCH(int, cursorPos); KPIMTextEdit::RichTextComposer edit; edit.createActions(new KActionCollection(this)); edit.setPlainText(initalText); QTextCursor cursor = edit.textCursor(); cursor.setPosition(cursorPos); edit.setTextCursor(cursor); edit.show(); // we need a layout for this to work edit.composerControler()->deleteCurrentLine(); QCOMPARE(edit.toPlainText(), expectedText); #endif } void RichTextComposerTest::testLoadImage() { KPIMTextEdit::RichTextComposer edit; edit.createActions(new KActionCollection(this)); QString image1Path = KIconLoader::global()->iconPath(QStringLiteral("folder-new"), KIconLoader::Small, false); QString image2Path = KIconLoader::global()->iconPath(QStringLiteral("arrow-up"), KIconLoader::Small, false); QImage image1, image2; QVERIFY(image1.load(image1Path)); QVERIFY(image2.load(image2Path)); edit.setHtml(QStringLiteral("BlaBla")); // First try to load an image with a name that doesn't match, it should fail edit.composerControler()->composerImages()->loadImage(image1, QStringLiteral("doesntmatch"), QStringLiteral("folder-new")); QVERIFY(!edit.document()->resource(QTextDocument::ImageResource, QUrl(QStringLiteral("folder-new"))).isValid()); // Now, load the image for real edit.composerControler()->composerImages()->loadImage(image1, QStringLiteral("folder-new.png"), QStringLiteral("folder-new")); QVERIFY(edit.document()->resource(QTextDocument::ImageResource, QUrl(QStringLiteral("folder-new"))).isValid()); // New test with a new textedit (so that we don't use the cached resources // This example has two images in the same text block, make sure that doesn't crash (bug 204214) KPIMTextEdit::RichTextComposer edit2; edit2.createActions(new KActionCollection(this)); edit2.setHtml(QStringLiteral("")); edit2.composerControler()->composerImages()->loadImage(image1, QStringLiteral("folder-new.png"), QStringLiteral("folder-new")); QVERIFY(edit.document()->resource(QTextDocument::ImageResource, QUrl(QStringLiteral("folder-new"))).isValid()); QCOMPARE(edit.composerControler()->composerImages()->embeddedImages().size(), 1); } void RichTextComposerTest::testWrappedPlainText_data() { QTest::addColumn("input"); QTest::addColumn("output"); QString defaultStr = QStringLiteral( "http://example.org/test-test-test-test-test-test-test-test-test-test-test-test-test\n https://example.org/test-test-test-test-test-test-test-test-test-test-test-test-test\ntest ftp://example.org/test-test-test-test-test-test-test-test-test-test-test-test-test\nftps://example.org/test-test-test-test-test-test-test-test-test-test-test-test-test\n ldap://example.org/test-test-test-test-test-test-test-test-test-test-test-test-test"); QTest::newRow("default") << defaultStr << defaultStr; QTest::newRow("empty") << QString() << QString(); QTest::newRow("wrap") << QStringLiteral("foosdfsdf sdsf sdfsdfsfs fsf sdfs df sfsdf dsf sdfsdf sf sf sfsdf sdsdf") << QStringLiteral( "foosdfsdf sdsf sdfsdfsfs fsf sdfs df sfsdf \ndsf sdfsdf sf sf sfsdf sdsdf"); QTest::newRow("wrap-2") << QStringLiteral("test-test-test-test-test-test-test-test-test-test-test-test-test") << QStringLiteral("test-test-test-test-test-test-test-test-\ntest-test-test-test-test"); QTest::newRow("wrap-3") << QStringLiteral("test-test-test-test-test-test-test-test-test-test-test-test-test\n\n") << QStringLiteral( "test-test-test-test-test-test-test-test-\ntest-test-test-test-test\n\n"); } void RichTextComposerTest::testWrappedPlainText() { QFETCH(QString, input); QFETCH(QString, output); KPIMTextEdit::RichTextComposer edit; edit.createActions(new KActionCollection(this)); edit.setPlainText(input); edit.show(); // < otherwise toWrappedPlainText can't work, it needs a layout QCOMPARE(edit.composerControler()->toWrappedPlainText(), output); } void RichTextComposerTest::testEnableDisableActions() { KPIMTextEdit::RichTextComposer composer; KActionCollection *actionCollection = new KActionCollection(&composer); composer.createActions(actionCollection); bool enableAction = true; composer.setEnableActions(enableAction); for (QAction *act : composer.actions()) { QCOMPARE(act->isEnabled(), enableAction); } enableAction = false; composer.setEnableActions(enableAction); for (QAction *act : composer.actions()) { QCOMPARE(act->isEnabled(), enableAction); } } void RichTextComposerTest::shouldHaveDefaultValue() { KPIMTextEdit::RichTextComposer composer; KActionCollection *actionCollection = new KActionCollection(&composer); composer.createActions(actionCollection); QCOMPARE(composer.linePosition(), 0); QCOMPARE(composer.columnNumber(), 0); QCOMPARE(composer.textMode(), KPIMTextEdit::RichTextComposer::Plain); QVERIFY(!composer.acceptRichText()); QVERIFY(!composer.quotePrefixName().isEmpty()); } void RichTextComposerTest::shouldChangeMode() { KPIMTextEdit::RichTextComposer composer; KActionCollection *actionCollection = new KActionCollection(&composer); composer.createActions(actionCollection); - QSignalSpy spy(&composer, SIGNAL(textModeChanged(KPIMTextEdit::RichTextComposer::Mode))); + QSignalSpy spy(&composer, &RichTextComposer::textModeChanged); composer.activateRichText(); QCOMPARE(composer.textMode(), KPIMTextEdit::RichTextComposer::Rich); QVERIFY(composer.acceptRichText()); QCOMPARE(spy.count(), 1); } QTEST_MAIN(RichTextComposerTest) diff --git a/src/composer-ng/richtextcomposercontroler.cpp b/src/composer-ng/richtextcomposercontroler.cpp index d8a2502..be35319 100644 --- a/src/composer-ng/richtextcomposercontroler.cpp +++ b/src/composer-ng/richtextcomposercontroler.cpp @@ -1,935 +1,935 @@ /* Copyright (C) 2015-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "richtextcomposer.h" #include "richtextcomposercontroler.h" #include "richtextcomposerimages.h" #include "klinkdialog_p.h" #include "nestedlisthelper_p.h" #include "inserthtmldialog.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "textutils.h" #include "insertimagedialog.h" using namespace KPIMTextEdit; class Q_DECL_HIDDEN RichTextComposerControler::RichTextComposerControlerPrivate { public: RichTextComposerControlerPrivate(RichTextComposer *composer, RichTextComposerControler *qq) : richtextComposer(composer) , q(qq) { nestedListHelper = new NestedListHelper(composer); richTextImages = new RichTextComposerImages(richtextComposer, q); } ~RichTextComposerControlerPrivate() { delete nestedListHelper; } QColor linkColor() { if (mLinkColor.isValid()) { return mLinkColor; } mLinkColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color(); return mLinkColor; } void selectLinkText(QTextCursor *cursor) const; void fixupTextEditString(QString &text) const; void mergeFormatOnWordOrSelection(const QTextCharFormat &format); QString addQuotesToText(const QString &inputText); void updateLink(const QString &linkUrl, const QString &linkText); QFont saveFont; QColor mLinkColor; QTextCharFormat painterFormat; NestedListHelper *nestedListHelper = nullptr; RichTextComposer *richtextComposer = nullptr; RichTextComposerImages *richTextImages = nullptr; RichTextComposerControler *q = nullptr; bool painterActive = false; }; void RichTextComposerControler::RichTextComposerControlerPrivate::selectLinkText(QTextCursor *cursor) const { // If the cursor is on a link, select the text of the link. if (cursor->charFormat().isAnchor()) { QString aHref = cursor->charFormat().anchorHref(); // Move cursor to start of link while (cursor->charFormat().anchorHref() == aHref) { if (cursor->atStart()) { break; } cursor->setPosition(cursor->position() - 1); } if (cursor->charFormat().anchorHref() != aHref) { cursor->setPosition(cursor->position() + 1, QTextCursor::KeepAnchor); } // Move selection to the end of the link while (cursor->charFormat().anchorHref() == aHref) { if (cursor->atEnd()) { break; } const int oldPosition = cursor->position(); cursor->movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor); //Wordaround Qt Bug. when we have a table. //FIXME selection url if (oldPosition == cursor->position()) { break; } } if (cursor->charFormat().anchorHref() != aHref) { cursor->setPosition(cursor->position() - 1, QTextCursor::KeepAnchor); } } else if (cursor->hasSelection()) { // Nothing to do. Using the currently selected text as the link text. } else { // Select current word cursor->movePosition(QTextCursor::StartOfWord); cursor->movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); } } void RichTextComposerControler::RichTextComposerControlerPrivate::mergeFormatOnWordOrSelection(const QTextCharFormat &format) { QTextCursor cursor = richtextComposer->textCursor(); QTextCursor wordStart(cursor); QTextCursor wordEnd(cursor); wordStart.movePosition(QTextCursor::StartOfWord); wordEnd.movePosition(QTextCursor::EndOfWord); cursor.beginEditBlock(); if (!cursor.hasSelection() && cursor.position() != wordStart.position() && cursor.position() != wordEnd.position()) { cursor.select(QTextCursor::WordUnderCursor); } cursor.mergeCharFormat(format); richtextComposer->mergeCurrentCharFormat(format); cursor.endEditBlock(); } RichTextComposerControler::RichTextComposerControler(RichTextComposer *richtextComposer, QObject *parent) : QObject(parent) , d(new RichTextComposerControlerPrivate(richtextComposer, this)) { } RichTextComposerControler::~RichTextComposerControler() { delete d; } bool RichTextComposerControler::painterActive() const { return d->painterActive; } void RichTextComposerControler::setFontForWholeText(const QFont &font) { QTextCharFormat fmt; fmt.setFont(font); QTextCursor cursor(richTextComposer()->document()); cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); cursor.mergeCharFormat(fmt); richTextComposer()->document()->setDefaultFont(font); } void RichTextComposerControler::disablePainter() { // If the painter is active, paint the selection with the // correct format. if (richTextComposer()->textCursor().hasSelection()) { QTextCursor cursor = richTextComposer()->textCursor(); cursor.setCharFormat(d->painterFormat); richTextComposer()->setTextCursor(cursor); } d->painterActive = false; } RichTextComposerImages *RichTextComposerControler::composerImages() const { return d->richTextImages; } NestedListHelper *RichTextComposerControler::nestedListHelper() const { return d->nestedListHelper; } void RichTextComposerControler::ensureCursorVisibleDelayed() { d->richtextComposer->ensureCursorVisible(); } RichTextComposer *RichTextComposerControler::richTextComposer() const { return d->richtextComposer; } void RichTextComposerControler::insertHorizontalRule() { QTextCursor cursor = richTextComposer()->textCursor(); QTextBlockFormat bf = cursor.blockFormat(); QTextCharFormat cf = cursor.charFormat(); cursor.beginEditBlock(); cursor.insertHtml(QStringLiteral("
")); cursor.insertBlock(bf, cf); richTextComposer()->setTextCursor(cursor); richTextComposer()->activateRichText(); cursor.endEditBlock(); } void RichTextComposerControler::alignLeft() { richTextComposer()->setAlignment(Qt::AlignLeft); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::alignCenter() { richTextComposer()->setAlignment(Qt::AlignHCenter); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::alignRight() { richTextComposer()->setAlignment(Qt::AlignRight); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::alignJustify() { richTextComposer()->setAlignment(Qt::AlignJustify); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::makeRightToLeft() { QTextBlockFormat format; format.setLayoutDirection(Qt::RightToLeft); QTextCursor cursor = richTextComposer()->textCursor(); cursor.mergeBlockFormat(format); richTextComposer()->setTextCursor(cursor); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::makeLeftToRight() { QTextBlockFormat format; format.setLayoutDirection(Qt::LeftToRight); QTextCursor cursor = richTextComposer()->textCursor(); cursor.mergeBlockFormat(format); richTextComposer()->setTextCursor(cursor); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setTextBold(bool bold) { QTextCharFormat fmt; fmt.setFontWeight(bold ? QFont::Bold : QFont::Normal); d->mergeFormatOnWordOrSelection(fmt); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setTextItalic(bool italic) { QTextCharFormat fmt; fmt.setFontItalic(italic); d->mergeFormatOnWordOrSelection(fmt); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setTextUnderline(bool underline) { QTextCharFormat fmt; fmt.setFontUnderline(underline); d->mergeFormatOnWordOrSelection(fmt); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setTextStrikeOut(bool strikeOut) { QTextCharFormat fmt; fmt.setFontStrikeOut(strikeOut); d->mergeFormatOnWordOrSelection(fmt); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setTextForegroundColor(const QColor &color) { QTextCharFormat fmt; fmt.setForeground(color); d->mergeFormatOnWordOrSelection(fmt); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setTextBackgroundColor(const QColor &color) { QTextCharFormat fmt; fmt.setBackground(color); d->mergeFormatOnWordOrSelection(fmt); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setFontFamily(const QString &fontFamily) { QTextCharFormat fmt; fmt.setFontFamily(fontFamily); d->mergeFormatOnWordOrSelection(fmt); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setFontSize(int size) { QTextCharFormat fmt; fmt.setFontPointSize(size); d->mergeFormatOnWordOrSelection(fmt); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setFont(const QFont &font) { QTextCharFormat fmt; fmt.setFont(font); d->mergeFormatOnWordOrSelection(fmt); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setTextSuperScript(bool superscript) { QTextCharFormat fmt; fmt.setVerticalAlignment(superscript ? QTextCharFormat::AlignSuperScript : QTextCharFormat::AlignNormal); d->mergeFormatOnWordOrSelection(fmt); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setTextSubScript(bool subscript) { QTextCharFormat fmt; fmt.setVerticalAlignment(subscript ? QTextCharFormat::AlignSubScript : QTextCharFormat::AlignNormal); d->mergeFormatOnWordOrSelection(fmt); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::setChangeTextForegroundColor() { const QColor currentColor = richTextComposer()->textColor(); const QColor defaultColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground().color(); const QColor selectedColor = QColorDialog::getColor(currentColor.isValid() ? currentColor : defaultColor, richTextComposer()); if (!selectedColor.isValid() && !currentColor.isValid()) { setTextForegroundColor(defaultColor); } else if (selectedColor.isValid()) { setTextForegroundColor(selectedColor); } } void RichTextComposerControler::setChangeTextBackgroundColor() { QTextCharFormat fmt = richTextComposer()->textCursor().charFormat(); const QColor currentColor = fmt.background().color(); const QColor defaultColor = KColorScheme(QPalette::Active, KColorScheme::View).foreground().color(); const QColor selectedColor = QColorDialog::getColor(currentColor.isValid() ? currentColor : defaultColor, richTextComposer()); if (!selectedColor.isValid() && !currentColor.isValid()) { setTextBackgroundColor(defaultColor); } else if (selectedColor.isValid()) { setTextBackgroundColor(selectedColor); } } QString RichTextComposerControler::currentLinkUrl() const { return richTextComposer()->textCursor().charFormat().anchorHref(); } QString RichTextComposerControler::currentLinkText() const { QTextCursor cursor = richTextComposer()->textCursor(); d->selectLinkText(&cursor); return cursor.selectedText(); } void RichTextComposerControler::selectLinkText() const { QTextCursor cursor = richTextComposer()->textCursor(); d->selectLinkText(&cursor); richTextComposer()->setTextCursor(cursor); } void RichTextComposerControler::manageLink() { selectLinkText(); QPointer linkDialog = new KLinkDialog(richTextComposer()); linkDialog->setLinkText(currentLinkText()); linkDialog->setLinkUrl(currentLinkUrl()); if (linkDialog->exec()) { d->updateLink(linkDialog->linkUrl(), linkDialog->linkText()); } delete linkDialog; } void RichTextComposerControler::updateLink(const QString &linkUrl, const QString &linkText) { d->updateLink(linkUrl, linkText); } void RichTextComposerControler::RichTextComposerControlerPrivate::updateLink(const QString &linkUrl, const QString &linkText) { q->selectLinkText(); QTextCursor cursor = richtextComposer->textCursor(); cursor.beginEditBlock(); if (!cursor.hasSelection()) { cursor.select(QTextCursor::WordUnderCursor); } QTextCharFormat format = cursor.charFormat(); // Save original format to create an extra space with the existing char // format for the block const QTextCharFormat originalFormat = format; if (!linkUrl.isEmpty()) { // Add link details format.setAnchor(true); format.setAnchorHref(linkUrl); // Workaround for QTBUG-1814: // Link formatting does not get applied immediately when setAnchor(true) // is called. So the formatting needs to be applied manually. format.setUnderlineStyle(QTextCharFormat::SingleUnderline); format.setUnderlineColor(linkColor()); format.setForeground(linkColor()); richtextComposer->activateRichText(); } else { // Remove link details format.setAnchor(false); format.setAnchorHref(QString()); // Workaround for QTBUG-1814: // Link formatting does not get removed immediately when setAnchor(false) // is called. So the formatting needs to be applied manually. QTextDocument defaultTextDocument; QTextCharFormat defaultCharFormat = defaultTextDocument.begin().charFormat(); format.setUnderlineStyle(defaultCharFormat.underlineStyle()); format.setUnderlineColor(defaultCharFormat.underlineColor()); format.setForeground(defaultCharFormat.foreground()); } // Insert link text specified in dialog, otherwise write out url. QString _linkText; if (!linkText.isEmpty()) { _linkText = linkText; } else { _linkText = linkUrl; } cursor.insertText(_linkText, format); // Insert a space after the link if at the end of the block so that // typing some text after the link does not carry link formatting if (!linkUrl.isEmpty() && cursor.atBlockEnd()) { cursor.setPosition(cursor.selectionEnd()); cursor.setCharFormat(originalFormat); cursor.insertText(QStringLiteral(" ")); } cursor.endEditBlock(); } QString RichTextComposerControler::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; } bool RichTextComposerControler::canIndentList() const { return d->nestedListHelper->canIndent(); } bool RichTextComposerControler::canDedentList() const { return d->nestedListHelper->canDedent(); } void RichTextComposerControler::indentListMore() { d->nestedListHelper->handleOnIndentMore(); richTextComposer()->activateRichText(); } void RichTextComposerControler::indentListLess() { d->nestedListHelper->handleOnIndentLess(); } void RichTextComposerControler::setListStyle(int styleIndex) { d->nestedListHelper->handleOnBulletType(-styleIndex); richTextComposer()->setFocus(); richTextComposer()->activateRichText(); } void RichTextComposerControler::insertLink(const QString &url) { if (url.isEmpty()) { return; } if (richTextComposer()->textMode() == RichTextComposer::Rich) { QTextCursor cursor = richTextComposer()->textCursor(); cursor.beginEditBlock(); QTextCharFormat format = cursor.charFormat(); // Save original format to create an extra space with the existing char // format for the block const QTextCharFormat originalFormat = format; // Add link details format.setAnchor(true); format.setAnchorHref(url); // Workaround for QTBUG-1814: // Link formatting does not get applied immediately when setAnchor(true) // is called. So the formatting needs to be applied manually. format.setUnderlineStyle(QTextCharFormat::SingleUnderline); format.setUnderlineColor(d->linkColor()); format.setForeground(d->linkColor()); // Insert link text specified in dialog, otherwise write out url. cursor.insertText(url, format); cursor.setPosition(cursor.selectionEnd()); cursor.setCharFormat(originalFormat); cursor.insertText(QStringLiteral(" \n")); cursor.endEditBlock(); } else { richTextComposer()->textCursor().insertText(url + QLatin1Char('\n')); } } void RichTextComposerControler::insertShareLink(const QString &url) { if (url.isEmpty()) { return; } const QString msg = i18n("I've linked 1 file to this email:"); if (richTextComposer()->textMode() == RichTextComposer::Rich) { QTextCursor cursor = richTextComposer()->textCursor(); cursor.beginEditBlock(); cursor.insertText(QLatin1Char('\n') + msg + QLatin1Char('\n')); QTextCharFormat format = cursor.charFormat(); // Save original format to create an extra space with the existing char // format for the block const QTextCharFormat originalFormat = format; // Add link details format.setAnchor(true); format.setAnchorHref(url); // Workaround for QTBUG-1814: // Link formatting does not get applied immediately when setAnchor(true) // is called. So the formatting needs to be applied manually. format.setUnderlineStyle(QTextCharFormat::SingleUnderline); format.setUnderlineColor(d->linkColor()); format.setForeground(d->linkColor()); // Insert link text specified in dialog, otherwise write out url. cursor.insertText(url, format); cursor.setPosition(cursor.selectionEnd()); cursor.setCharFormat(originalFormat); cursor.insertText(QStringLiteral(" \n")); cursor.endEditBlock(); } else { richTextComposer()->textCursor().insertText(QLatin1Char('\n') + msg + QLatin1Char('\n') + url + QLatin1Char('\n')); } } void RichTextComposerControler::setCursorPositionFromStart(unsigned int pos) { if (pos > 0) { QTextCursor cursor = richTextComposer()->textCursor(); //Fix html pos cursor cursor.setPosition(qMin(pos, (unsigned int)cursor.document()->characterCount() - 1)); richTextComposer()->setTextCursor(cursor); ensureCursorVisible(); } } void RichTextComposerControler::ensureCursorVisible() { // Hack: In KMail, the layout of the composer changes again after // creating the editor (the toolbar/menubar creation is delayed), so // the size of the editor changes as well, possibly hiding the cursor // even though we called ensureCursorVisible() before the layout phase. // // Delay the actual call to ensureCursorVisible() a bit to work around // the problem. QTimer::singleShot(500, richTextComposer()->composerControler(), &RichTextComposerControler::ensureCursorVisibleDelayed); } void RichTextComposerControler::RichTextComposerControlerPrivate::fixupTextEditString(QString &text) const { // Remove line separators. Normal \n chars are still there, so no linebreaks get lost here text.remove(QChar::LineSeparator); // Get rid of embedded images, see QTextImageFormat documentation: // "Inline images are represented by an object replacement character (0xFFFC in Unicode) " text.remove(0xFFFC); // In plaintext mode, each space is non-breaking. text.replace(QChar::Nbsp, QChar::fromLatin1(' ')); } bool RichTextComposerControler::isFormattingUsed() const { if (richTextComposer()->textMode() == RichTextComposer::Plain) { return false; } return KPIMTextEdit::TextUtils::containsFormatting(richTextComposer()->document()); } void RichTextComposerControler::slotAddEmoticon(const QString &text) { QTextCursor cursor = richTextComposer()->textCursor(); cursor.insertText(text); } void RichTextComposerControler::slotInsertHtml() { if (richTextComposer()->textMode() == RichTextComposer::Rich) { QPointer dialog = new KPIMTextEdit::InsertHtmlDialog(richTextComposer()); const QTextDocumentFragment fragmentSelected = richTextComposer()->textCursor().selection(); if (!fragmentSelected.isEmpty()) { dialog->setSelectedText(fragmentSelected.toHtml()); } if (dialog->exec()) { const QString str = dialog->html(); if (!str.isEmpty()) { QTextCursor cursor = richTextComposer()->textCursor(); cursor.insertHtml(str); } } delete dialog; } } void RichTextComposerControler::slotAddImage() { QPointer dlg = new KPIMTextEdit::InsertImageDialog(richTextComposer()); if (dlg->exec() == QDialog::Accepted) { const QUrl url = dlg->imageUrl(); int imageWidth = -1; int imageHeight = -1; if (!dlg->keepOriginalSize()) { imageWidth = dlg->imageWidth(); imageHeight = dlg->imageHeight(); } if (url.isLocalFile()) { d->richTextImages->addImage(url, imageWidth, imageHeight); } else { KMessageBox::error(richTextComposer(), i18n("Only local files are supported.")); } } delete dlg; } void RichTextComposerControler::slotFormatReset() { setTextBackgroundColor(richTextComposer()->palette().highlightedText().color()); setTextForegroundColor(richTextComposer()->palette().text().color()); richTextComposer()->setFont(d->saveFont); } void RichTextComposerControler::slotDeleteLine() { if (richTextComposer()->hasFocus()) { QTextCursor cursor = richTextComposer()->textCursor(); QTextBlock block = cursor.block(); const QTextLayout *layout = block.layout(); // The current text block can have several lines due to word wrapping. // Search the line the cursor is in, and then delete it. for (int lineNumber = 0; lineNumber < layout->lineCount(); ++lineNumber) { QTextLine line = layout->lineAt(lineNumber); const bool lastLineInBlock = (line.textStart() + line.textLength() == block.length() - 1); const bool oneLineBlock = (layout->lineCount() == 1); const int startOfLine = block.position() + line.textStart(); int endOfLine = block.position() + line.textStart() + line.textLength(); if (!lastLineInBlock) { endOfLine -= 1; } // Found the line where the cursor is in if (cursor.position() >= startOfLine && cursor.position() <= endOfLine) { int deleteStart = startOfLine; int deleteLength = line.textLength(); if (oneLineBlock) { deleteLength++; // The trailing newline } // When deleting the last line in the document, // remove the newline of the line before the last line instead if (deleteStart + deleteLength >= richTextComposer()->document()->characterCount() && deleteStart > 0) { deleteStart--; } cursor.beginEditBlock(); cursor.setPosition(deleteStart); cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, deleteLength); cursor.removeSelectedText(); cursor.endEditBlock(); return; } } } } void RichTextComposerControler::slotPasteAsQuotation() { #ifndef QT_NO_CLIPBOARD if (richTextComposer()->hasFocus()) { const QString s = QApplication::clipboard()->text(); if (!s.isEmpty()) { richTextComposer()->insertPlainText(d->addQuotesToText(s)); } } #endif } void RichTextComposerControler::slotPasteWithoutFormatting() { #ifndef QT_NO_CLIPBOARD if (richTextComposer()->hasFocus()) { const QString s = QApplication::clipboard()->text(); if (!s.isEmpty()) { richTextComposer()->insertPlainText(s); } } #endif } void RichTextComposerControler::slotRemoveQuotes() { QTextCursor cursor = richTextComposer()->textCursor(); cursor.beginEditBlock(); if (!cursor.hasSelection()) { cursor.select(QTextCursor::Document); } QTextBlock block = richTextComposer()->document()->findBlock(cursor.selectionStart()); int selectionEnd = cursor.selectionEnd(); while (block.isValid() && block.position() <= selectionEnd) { cursor.setPosition(block.position()); const int length = richTextComposer()->quoteLength(block.text(), true); if (length > 0) { cursor.movePosition(QTextCursor::NextCharacter, QTextCursor::KeepAnchor, length); cursor.removeSelectedText(); selectionEnd -= length; } block = block.next(); } cursor.clearSelection(); cursor.endEditBlock(); } void RichTextComposerControler::slotAddQuotes() { QTextCursor cursor = richTextComposer()->textCursor(); cursor.beginEditBlock(); QString selectedText; bool lastCharacterIsAParagraphChar = false; if (!cursor.hasSelection()) { cursor.select(QTextCursor::Document); selectedText = cursor.selectedText(); cursor.removeSelectedText(); } else { selectedText = cursor.selectedText(); if (selectedText[selectedText.length() - 1] == QChar::ParagraphSeparator) { lastCharacterIsAParagraphChar = true; } } richTextComposer()->insertPlainText(d->addQuotesToText(selectedText) + (lastCharacterIsAParagraphChar ? QChar::ParagraphSeparator : QChar())); cursor.endEditBlock(); } QString RichTextComposerControler::RichTextComposerControlerPrivate::addQuotesToText(const QString &inputText) { QString answer = inputText; const QString indentStr = richtextComposer->defaultQuoteSign(); answer.replace(QLatin1Char('\n'), QLatin1Char('\n') + indentStr); //cursor.selectText() as QChar::ParagraphSeparator as paragraph separator. answer.replace(QChar::ParagraphSeparator, QLatin1Char('\n') + indentStr); answer.prepend(indentStr); answer += QLatin1Char('\n'); return richtextComposer->smartQuote(answer); } void RichTextComposerControler::slotFormatPainter(bool active) { if (active) { d->painterFormat = richTextComposer()->currentCharFormat(); d->painterActive = true; richTextComposer()->viewport()->setCursor(QCursor(QIcon::fromTheme(QStringLiteral("draw-brush")).pixmap(32, 32), 0, 32)); } else { d->painterFormat = QTextCharFormat(); d->painterActive = false; richTextComposer()->viewport()->setCursor(Qt::IBeamCursor); } } void RichTextComposerControler::textModeChanged(KPIMTextEdit::RichTextComposer::Mode mode) { if (mode == KPIMTextEdit::RichTextComposer::Rich) { d->saveFont = richTextComposer()->currentFont(); } } QString RichTextComposerControler::toCleanPlainText(const QString &plainText) const { QString temp = plainText.isEmpty() ? richTextComposer()->toPlainText() : plainText; d->fixupTextEditString(temp); return temp; } QString RichTextComposerControler::toWrappedPlainText() const { QTextDocument *doc = richTextComposer()->document(); return toWrappedPlainText(doc); } QString RichTextComposerControler::toWrappedPlainText(QTextDocument *doc) const { QString temp; - QRegularExpression rx(QLatin1String("(http|ftp|ldap)s?\\S+-$")); + QRegularExpression rx(QStringLiteral("(http|ftp|ldap)s?\\S+-$")); QTextBlock block = doc->begin(); while (block.isValid()) { QTextLayout *layout = block.layout(); const int numberOfLine(layout->lineCount()); bool urlStart = false; for (int i = 0; i < numberOfLine; ++i) { QTextLine line = layout->lineAt(i); QString lineText = block.text().mid(line.textStart(), line.textLength()); if (lineText.contains(rx) || (urlStart && !lineText.contains(QLatin1Char(' ')) && lineText.endsWith(QLatin1Char('-')))) { // don't insert line break in URL temp += lineText; urlStart = true; } else { temp += lineText + QLatin1Char('\n'); } } block = block.next(); } // Remove the last superfluous newline added above if (temp.endsWith(QLatin1Char('\n'))) { temp.chop(1); } d->fixupTextEditString(temp); return temp; } diff --git a/src/grantleebuilder/plaintextmarkupbuilder.cpp b/src/grantleebuilder/plaintextmarkupbuilder.cpp index 2b11215..9a268a8 100644 --- a/src/grantleebuilder/plaintextmarkupbuilder.cpp +++ b/src/grantleebuilder/plaintextmarkupbuilder.cpp @@ -1,490 +1,490 @@ /* Copyright (c) 2019 Montel Laurent This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "plaintextmarkupbuilder.h" #include namespace KPIMTextEdit { class PlainTextMarkupBuilderPrivate { public: PlainTextMarkupBuilderPrivate(PlainTextMarkupBuilder *b) : q_ptr(b) {} /** Get a letter string to represent a number. The numbers 1-26 are represented by a-z, and 27-52 by aa-az, 53-79 by ba-bz etc. @param The number to convert @return The letter string representation of the number. */ QString getLetterString(int itemNumber); QString getRomanString(int itemNumber); /** Gets a block of references in the body of the text. This is an ordered list of links and images in the text. */ QString getReferences(); QStringList m_urls; QList currentListItemStyles; QList currentListItemNumbers; QString activeLink; QString m_text; QString m_quoteprefix; PlainTextMarkupBuilder *q_ptr; Q_DECLARE_PUBLIC(PlainTextMarkupBuilder) }; } using namespace KPIMTextEdit; PlainTextMarkupBuilder::PlainTextMarkupBuilder() : d_ptr(new PlainTextMarkupBuilderPrivate(this)) { } QString PlainTextMarkupBuilderPrivate::getRomanString(int item) { QString result; // Code based to gui/text/qtextlist.cpp if (item < 5000) { QString romanNumeral; // works for up to 4999 items auto romanSymbols = QStringLiteral("iiivixxxlxcccdcmmmm"); int c[] = {1, 4, 5, 9, 10, 40, 50, 90, 100, 400, 500, 900, 1000}; auto n = item; for (auto i = 12; i >= 0; n %= c[i], i--) { auto q = n / c[i]; if (q > 0) { auto startDigit = i + (i + 3) / 4; int numDigits; if (i % 4) { // c[i] == 4|5|9|40|50|90|400|500|900 if ((i - 2) % 4) { // c[i] == 4|9|40|90|400|900 => with subtraction (IV, // IX, XL, XC, // ...) numDigits = 2; } else { // c[i] == 5|50|500 (V, L, D) numDigits = 1; } } else { // c[i] == 1|10|100|1000 (I, II, III, X, XX, ...) numDigits = q; } - romanNumeral.append(romanSymbols.mid(startDigit, numDigits)); + romanNumeral.append(romanSymbols.midRef(startDigit, numDigits)); } } result = romanNumeral; } else { result = QStringLiteral("?"); } return result; } QString PlainTextMarkupBuilderPrivate::getLetterString(int itemNumber) { QString letterString; while (true) { // Create the letter string by prepending one char at a time. // The itemNumber is converted to a number in the base 36 (number of // letters // in the // alphabet plus 10) after being increased by 10 (to pass out the digits // 0 // to 9). letterString.prepend(QStringLiteral("%1").arg( (itemNumber % LETTERSINALPHABET) + DIGITSOFFSET, 0, // no padding while building this string. LETTERSINALPHABET + DIGITSOFFSET)); if ((itemNumber >= LETTERSINALPHABET)) { itemNumber = itemNumber / LETTERSINALPHABET; itemNumber--; } else { break; } } return letterString; } QString PlainTextMarkupBuilderPrivate::getReferences() { QString refs; if (!m_urls.isEmpty()) { refs.append(QStringLiteral("\n--------\n")); auto index = 1; while (!m_urls.isEmpty()) { refs.append( QStringLiteral("[%1] %2\n").arg(index++).arg(m_urls.takeFirst())); } } return refs; } PlainTextMarkupBuilder::~PlainTextMarkupBuilder() { delete d_ptr; } void PlainTextMarkupBuilder::setQuotePrefix(const QString &prefix) { Q_D(PlainTextMarkupBuilder); d->m_quoteprefix = prefix; } void PlainTextMarkupBuilder::beginStrong() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('*')); } void PlainTextMarkupBuilder::endStrong() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('*')); } void PlainTextMarkupBuilder::beginEmph() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('/')); } void PlainTextMarkupBuilder::endEmph() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('/')); } void PlainTextMarkupBuilder::beginUnderline() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('_')); } void PlainTextMarkupBuilder::endUnderline() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('_')); } void PlainTextMarkupBuilder::beginStrikeout() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('-')); } void PlainTextMarkupBuilder::endStrikeout() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('-')); } void PlainTextMarkupBuilder::beginAnchor(const QString &href, const QString &name) { Q_D(PlainTextMarkupBuilder); Q_UNUSED(name); if (!d->m_urls.contains(href)) { d->m_urls.append(href); } d->activeLink = href; } void PlainTextMarkupBuilder::endAnchor() { Q_D(PlainTextMarkupBuilder); d->m_text.append( QStringLiteral("[%1]").arg(d->m_urls.indexOf(d->activeLink) + 1)); } void PlainTextMarkupBuilder::endParagraph() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('\n')); } void PlainTextMarkupBuilder::addNewline() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('\n')); } void PlainTextMarkupBuilder::insertHorizontalRule(int width) { Q_UNUSED(width) Q_D(PlainTextMarkupBuilder); d->m_text.append(QStringLiteral("--------------------\n")); } int PlainTextMarkupBuilder::addReference(const QString &reference) { Q_D(PlainTextMarkupBuilder); if (!d->m_urls.contains(reference)) d->m_urls.append(reference); return d->m_urls.indexOf(reference) + 1; } void PlainTextMarkupBuilder::insertImage(const QString &src, qreal width, qreal height) { Q_D(PlainTextMarkupBuilder); Q_UNUSED(width) Q_UNUSED(height) auto ref = addReference(src); d->m_text.append(QStringLiteral("[%1]").arg(ref)); } void PlainTextMarkupBuilder::beginList(QTextListFormat::Style style) { Q_D(PlainTextMarkupBuilder); d->currentListItemStyles.append(style); d->currentListItemNumbers.append(0); } void PlainTextMarkupBuilder::endList() { Q_D(PlainTextMarkupBuilder); if (!d->currentListItemNumbers.isEmpty()) { d->currentListItemStyles.removeLast(); d->currentListItemNumbers.removeLast(); } } void PlainTextMarkupBuilder::beginListItem() { Q_D(PlainTextMarkupBuilder); for (auto i = 0; i < d->currentListItemNumbers.size(); i++) { d->m_text.append(QStringLiteral(" ")); } auto itemNumber = d->currentListItemNumbers.last(); switch (d->currentListItemStyles.last()) { case QTextListFormat::ListDisc: d->m_text.append(QStringLiteral(" * ")); break; case QTextListFormat::ListCircle: d->m_text.append(QStringLiteral(" o ")); break; case QTextListFormat::ListSquare: d->m_text.append(QStringLiteral(" - ")); break; case QTextListFormat::ListDecimal: d->m_text.append(QStringLiteral(" %1. ").arg(itemNumber + 1)); break; case QTextListFormat::ListLowerAlpha: d->m_text.append( QStringLiteral(" %1. ").arg(d->getLetterString(itemNumber))); break; case QTextListFormat::ListUpperAlpha: d->m_text.append( QStringLiteral(" %1. ").arg(d->getLetterString(itemNumber).toUpper())); break; case QTextListFormat::ListLowerRoman: d->m_text.append( QStringLiteral(" %1. ").arg(d->getRomanString(itemNumber + 1))); break; case QTextListFormat::ListUpperRoman: d->m_text.append(QStringLiteral(" %1. ").arg( d->getRomanString(itemNumber + 1).toUpper())); break; default: break; } } void PlainTextMarkupBuilder::endListItem() { Q_D(PlainTextMarkupBuilder); d->currentListItemNumbers.last() = d->currentListItemNumbers.last() + 1; d->m_text.append(QLatin1Char('\n')); } void PlainTextMarkupBuilder::beginSuperscript() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QStringLiteral("^{")); } void PlainTextMarkupBuilder::endSuperscript() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('}')); } void PlainTextMarkupBuilder::beginSubscript() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QStringLiteral("_{")); } void PlainTextMarkupBuilder::endSubscript() { Q_D(PlainTextMarkupBuilder); d->m_text.append(QLatin1Char('}')); } void PlainTextMarkupBuilder::appendLiteralText(const QString &text) { Q_D(PlainTextMarkupBuilder); d->m_text.append(text); } void PlainTextMarkupBuilder::appendRawText(const QString &text) { Q_D(PlainTextMarkupBuilder); d->m_text.append(text); } QString PlainTextMarkupBuilder::getResult() { Q_D(PlainTextMarkupBuilder); auto ret = d->m_text; ret.append(d->getReferences()); d->m_text.clear(); return ret; } void PlainTextMarkupBuilder::beginParagraph(Qt::Alignment a, qreal top, qreal bottom, qreal left, qreal right) { Q_UNUSED(a); Q_D(PlainTextMarkupBuilder); if (isQuoteBlock(top, bottom, left, right)) { d->m_text.append(d->m_quoteprefix); } } bool PlainTextMarkupBuilder::isQuoteBlock(qreal top, qreal bottom, qreal left, qreal right) const { Q_UNUSED(top); Q_UNUSED(bottom); return /*(top == 12) && (bottom == 12) &&*/ (left == 40) && (right == 40); } void PlainTextMarkupBuilder::beginBackground(const QBrush &brush) { Q_UNUSED(brush); } void PlainTextMarkupBuilder::beginFontFamily(const QString &family) { Q_UNUSED(family); } void PlainTextMarkupBuilder::beginFontPointSize(int size) { Q_UNUSED(size); } void PlainTextMarkupBuilder::beginForeground(const QBrush &brush) { Q_UNUSED(brush); } void PlainTextMarkupBuilder::beginHeader(int level) { Q_UNUSED(level); } void PlainTextMarkupBuilder::beginTable(qreal cellpadding, qreal cellspacing, const QString &width) { Q_UNUSED(cellpadding); Q_UNUSED(cellspacing); Q_UNUSED(width); } void PlainTextMarkupBuilder::beginTableCell(const QString &width, int colSpan, int rowSpan) { Q_UNUSED(width); Q_UNUSED(colSpan); Q_UNUSED(rowSpan); } void PlainTextMarkupBuilder::beginTableHeaderCell(const QString &width, int colSpan, int rowSpan) { Q_UNUSED(width); Q_UNUSED(colSpan); Q_UNUSED(rowSpan); } void PlainTextMarkupBuilder::beginTableRow() { } void PlainTextMarkupBuilder::endBackground() { } void PlainTextMarkupBuilder::endFontFamily() { } void PlainTextMarkupBuilder::endFontPointSize() { } void PlainTextMarkupBuilder::endForeground() { } void PlainTextMarkupBuilder::endHeader(int level) { Q_UNUSED(level) } void PlainTextMarkupBuilder::endTable() { } void PlainTextMarkupBuilder::endTableCell() { } void PlainTextMarkupBuilder::endTableHeaderCell() { } void PlainTextMarkupBuilder::endTableRow() { } diff --git a/src/texteditor/commonwidget/autotests/textgotolinewidgettest.cpp b/src/texteditor/commonwidget/autotests/textgotolinewidgettest.cpp index 2d3b6c1..d9c81e7 100644 --- a/src/texteditor/commonwidget/autotests/textgotolinewidgettest.cpp +++ b/src/texteditor/commonwidget/autotests/textgotolinewidgettest.cpp @@ -1,156 +1,156 @@ /* Copyright (C) 2014-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "textgotolinewidgettest.h" #include "texteditor/commonwidget/textgotolinewidget.h" #include #include #include #include #include #include #include TextGoToLineWidgetTest::TextGoToLineWidgetTest(QObject *parent) : QObject(parent) { } void TextGoToLineWidgetTest::shouldHaveDefaultValuesOnCreation() { KPIMTextEdit::TextGoToLineWidget edit; edit.show(); QVERIFY(QTest::qWaitForWindowExposed(&edit)); QSpinBox *line = edit.findChild(QStringLiteral("line")); QVERIFY(line); QCOMPARE(line->minimum(), 1); QPushButton *gotolinebutton = edit.findChild(QStringLiteral("gotoline")); QVERIFY(gotolinebutton); QToolButton *closebutton = edit.findChild(QStringLiteral("closebutton")); QVERIFY(closebutton); QVERIFY(line->hasFocus()); } void TextGoToLineWidgetTest::shouldEmitGoToLineSignalWhenPressOnButton() { KPIMTextEdit::TextGoToLineWidget edit; QPushButton *gotolinebutton = edit.findChild(QStringLiteral("gotoline")); - QSignalSpy spy(&edit, SIGNAL(moveToLine(int))); + QSignalSpy spy(&edit, &KPIMTextEdit::TextGoToLineWidget::moveToLine); QTest::mouseClick(gotolinebutton, Qt::LeftButton); QCOMPARE(spy.count(), 1); } void TextGoToLineWidgetTest::shouldEmitGoToLineSignalCorrectValueWhenPressOnButton() { KPIMTextEdit::TextGoToLineWidget edit; QPushButton *gotolinebutton = edit.findChild(QStringLiteral("gotoline")); QSpinBox *line = edit.findChild(QStringLiteral("line")); line->setValue(5); QCOMPARE(line->value(), 5); - QSignalSpy spy(&edit, SIGNAL(moveToLine(int))); + QSignalSpy spy(&edit, &KPIMTextEdit::TextGoToLineWidget::moveToLine); QTest::mouseClick(gotolinebutton, Qt::LeftButton); QCOMPARE(spy.count(), 1); QCOMPARE(spy.at(0).at(0).toInt(), 5); } void TextGoToLineWidgetTest::shouldHideWidgetWhenClickOnCloseButton() { KPIMTextEdit::TextGoToLineWidget edit; edit.show(); QVERIFY(QTest::qWaitForWindowExposed(&edit)); QVERIFY(edit.isVisible()); QToolButton *closebutton = edit.findChild(QStringLiteral("closebutton")); QTest::mouseClick(closebutton, Qt::LeftButton); QVERIFY(!edit.isVisible()); } void TextGoToLineWidgetTest::shouldHideWidgetWhenPressEscape() { KPIMTextEdit::TextGoToLineWidget edit; edit.show(); QVERIFY(QTest::qWaitForWindowExposed(&edit)); QTest::keyPress(&edit, Qt::Key_Escape); QCOMPARE(edit.isVisible(), false); } void TextGoToLineWidgetTest::shouldEmitGoToLineSignalWhenSpinboxHasFocusAndWePressEnter() { KPIMTextEdit::TextGoToLineWidget edit; edit.show(); QVERIFY(QTest::qWaitForWindowExposed(&edit)); QSpinBox *line = edit.findChild(QStringLiteral("line")); line->setFocus(); QVERIFY(line->hasFocus()); line->setValue(5); - QSignalSpy spy(&edit, SIGNAL(moveToLine(int))); + QSignalSpy spy(&edit, &KPIMTextEdit::TextGoToLineWidget::moveToLine); QTest::keyPress(line, Qt::Key_Enter); QCOMPARE(spy.count(), 1); QCOMPARE(spy.at(0).at(0).toInt(), 5); } void TextGoToLineWidgetTest::shouldHasFocusEachTimeThatItShown() { KPIMTextEdit::TextGoToLineWidget edit; edit.show(); QVERIFY(QTest::qWaitForWindowExposed(&edit)); QSpinBox *line = edit.findChild(QStringLiteral("line")); QVERIFY(line); QVERIFY(line->hasFocus()); edit.hide(); QVERIFY(!line->hasFocus()); edit.show(); QVERIFY(QTest::qWaitForWindowExposed(&edit)); //FIXME QVERIFY(line->hasFocus()); } void TextGoToLineWidgetTest::shouldSetFocusWhenWeRecallGotToLine() { KPIMTextEdit::TextGoToLineWidget edit; edit.show(); QVERIFY(QTest::qWaitForWindowExposed(&edit)); QSpinBox *line = edit.findChild(QStringLiteral("line")); QVERIFY(line->hasFocus()); edit.setFocus(); QVERIFY(!line->hasFocus()); edit.goToLine(); QVERIFY(line->hasFocus()); } void TextGoToLineWidgetTest::shouldChangeMaximumValue() { KPIMTextEdit::TextGoToLineWidget edit; edit.show(); QVERIFY(QTest::qWaitForWindowExposed(&edit)); QSpinBox *line = edit.findChild(QStringLiteral("line")); QCOMPARE(line->value(), 1); QCOMPARE(line->minimum(), 1); edit.setMaximumLineCount(50); QCOMPARE(line->value(), 1); QCOMPARE(line->minimum(), 1); QCOMPARE(line->maximum(), 50); edit.setMaximumLineCount(10); QCOMPARE(line->value(), 1); QCOMPARE(line->minimum(), 1); QCOMPARE(line->maximum(), 10); } QTEST_MAIN(TextGoToLineWidgetTest) diff --git a/src/texteditor/plaintexteditor/plaintexteditor.cpp b/src/texteditor/plaintexteditor/plaintexteditor.cpp index 5054eda..97b9496 100644 --- a/src/texteditor/plaintexteditor/plaintexteditor.cpp +++ b/src/texteditor/plaintexteditor/plaintexteditor.cpp @@ -1,892 +1,892 @@ /* Copyright (C) 2013-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "plaintexteditor.h" #include "kpimtextedit_debug.h" #include "texteditor/commonwidget/textmessageindicator.h" #include #include #include #include #include #include #include #include #include #include #include #include #include "texttospeech/texttospeech.h" #include #include #include #include #include #include #include #include #include #include #include using namespace KPIMTextEdit; class Q_DECL_HIDDEN PlainTextEditor::PlainTextEditorPrivate { public: PlainTextEditorPrivate(PlainTextEditor *qq) : q(qq) , mTextIndicator(new KPIMTextEdit::TextMessageIndicator(q)) , webshortcutMenuManager(new KIO::KUriFilterSearchProviderActions(q)) { KConfig sonnetKConfig(QStringLiteral("sonnetrc")); KConfigGroup group(&sonnetKConfig, "Spelling"); checkSpellingEnabled = group.readEntry("checkerEnabledByDefault", false); supportFeatures |= PlainTextEditor::Search; supportFeatures |= PlainTextEditor::SpellChecking; supportFeatures |= PlainTextEditor::TextToSpeech; supportFeatures |= PlainTextEditor::AllowWebShortcut; } ~PlainTextEditorPrivate() { delete richTextDecorator; delete speller; } QStringList ignoreSpellCheckingWords; PlainTextEditor *q; KPIMTextEdit::TextMessageIndicator *mTextIndicator = nullptr; KIO::KUriFilterSearchProviderActions *webshortcutMenuManager = nullptr; Sonnet::SpellCheckDecorator *richTextDecorator = nullptr; Sonnet::Speller *speller = nullptr; QString spellCheckingConfigFileName; QString spellCheckingLanguage; QTextDocumentFragment originalDoc; PlainTextEditor::SupportFeatures supportFeatures; int mInitialFontSize = 0; bool customPalette = false; bool activateLanguageMenu = true; bool checkSpellingEnabled = false; }; PlainTextEditor::PlainTextEditor(QWidget *parent) : QPlainTextEdit(parent) , d(new PlainTextEditor::PlainTextEditorPrivate(this)) { KCursor::setAutoHideCursor(this, true, false); setSpellCheckingConfigFileName(QString()); d->mInitialFontSize = font().pointSize(); } PlainTextEditor::~PlainTextEditor() { delete d; } void PlainTextEditor::addIgnoreWords(const QStringList &lst) { d->ignoreSpellCheckingWords = lst; addIgnoreWordsToHighLighter(); } void PlainTextEditor::slotDisplayMessageIndicator(const QString &message) { d->mTextIndicator->display(message); } void PlainTextEditor::contextMenuEvent(QContextMenuEvent *event) { QMenu *popup = createStandardContextMenu(); if (popup) { const bool emptyDocument = document()->isEmpty(); if (!isReadOnly()) { QList actionList = popup->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs }; QAction *separatorAction = nullptr; const int idx = actionList.indexOf(actionList[SelectAllAct]) + 1; if (idx < actionList.count()) { separatorAction = actionList.at(idx); } if (separatorAction) { if (!emptyDocument) { QAction *clearAllAction = KStandardAction::clear(this, &PlainTextEditor::slotUndoableClear, popup); popup->insertAction(separatorAction, clearAllAction); } } } KIconTheme::assignIconsToContextMenu(isReadOnly() ? KIconTheme::ReadOnlyText : KIconTheme::TextEditor, popup->actions()); if (d->supportFeatures & Search) { popup->addSeparator(); if (!emptyDocument) { - popup->addAction(KStandardGuiItem::find().icon(), KStandardGuiItem::find().text(), this, SIGNAL(findText()), Qt::Key_F + Qt::CTRL); + popup->addAction(KStandardGuiItem::find().icon(), KStandardGuiItem::find().text(), this, &PlainTextEditor::findText, Qt::Key_F + Qt::CTRL); popup->addSeparator(); } if (!isReadOnly()) { if (!emptyDocument) { - popup->addAction(i18n("Replace..."), this, SIGNAL(replaceText()), Qt::Key_R + Qt::CTRL); + popup->addAction(i18n("Replace..."), this, &PlainTextEditor::replaceText, Qt::Key_R + Qt::CTRL); popup->addSeparator(); } } } else { popup->addSeparator(); } if (!isReadOnly() && spellCheckingSupport()) { if (!d->speller) { d->speller = new Sonnet::Speller(); } if (!d->speller->availableBackends().isEmpty()) { if (!emptyDocument) { popup->addAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("Check Spelling..."), this, &PlainTextEditor::slotCheckSpelling); popup->addSeparator(); } QAction *autoSpellCheckAction = popup->addAction(i18n("Auto Spell Check"), this, &PlainTextEditor::slotToggleAutoSpellCheck); autoSpellCheckAction->setCheckable(true); autoSpellCheckAction->setChecked(checkSpellingEnabled()); popup->addAction(autoSpellCheckAction); if (checkSpellingEnabled() && d->activateLanguageMenu) { QMenu *languagesMenu = new QMenu(i18n("Spell Checking Language"), popup); QActionGroup *languagesGroup = new QActionGroup(languagesMenu); languagesGroup->setExclusive(true); QString defaultSpellcheckingLanguage = spellCheckingLanguage(); if (defaultSpellcheckingLanguage.isEmpty()) { //TODO fix default value defaultSpellcheckingLanguage = d->speller->defaultLanguage(); } QMapIterator i(d->speller->availableDictionaries()); while (i.hasNext()) { i.next(); QAction *languageAction = languagesMenu->addAction(i.key()); languageAction->setCheckable(true); languageAction->setChecked(defaultSpellcheckingLanguage == i.value()); languageAction->setData(i.value()); languageAction->setActionGroup(languagesGroup); connect(languageAction, &QAction::triggered, this, &PlainTextEditor::slotLanguageSelected); } popup->addMenu(languagesMenu); } popup->addSeparator(); } } if (d->supportFeatures & TextToSpeech) { if (KPIMTextEdit::TextToSpeech::self()->isReady()) { if (!emptyDocument) { QAction *speakAction = popup->addAction(i18n("Speak Text")); speakAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech"))); connect(speakAction, &QAction::triggered, this, &PlainTextEditor::slotSpeakText); } } } if (webShortcutSupport() && textCursor().hasSelection()) { popup->addSeparator(); const QString selectedText = textCursor().selectedText(); d->webshortcutMenuManager->setSelectedText(selectedText); d->webshortcutMenuManager->addWebShortcutsToMenu(popup); } addExtraMenuEntry(popup, event->pos()); popup->exec(event->globalPos()); delete popup; } } void PlainTextEditor::addExtraMenuEntry(QMenu *menu, QPoint pos) { Q_UNUSED(menu); Q_UNUSED(pos); } void PlainTextEditor::slotSpeakText() { QString text; if (textCursor().hasSelection()) { text = textCursor().selectedText(); } else { text = toPlainText(); } //qCDebug(KPIMTEXTEDIT_LOG) << " KPIMTextEdit::TextToSpeech::self()->isReady() :" << KPIMTextEdit::TextToSpeech::self()->isReady(); Q_EMIT say(text); } void PlainTextEditor::slotUndoableClear() { QTextCursor cursor = textCursor(); cursor.beginEditBlock(); cursor.movePosition(QTextCursor::Start); cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); cursor.removeSelectedText(); cursor.endEditBlock(); } void PlainTextEditor::setSearchSupport(bool b) { if (b) { d->supportFeatures |= Search; } else { d->supportFeatures = (d->supportFeatures & ~Search); } } bool PlainTextEditor::searchSupport() const { return d->supportFeatures & Search; } void PlainTextEditor::setTextToSpeechSupport(bool b) { if (b) { d->supportFeatures |= TextToSpeech; } else { d->supportFeatures = (d->supportFeatures & ~TextToSpeech); } } bool PlainTextEditor::textToSpeechSupport() const { return d->supportFeatures & TextToSpeech; } bool PlainTextEditor::spellCheckingSupport() const { return d->supportFeatures & SpellChecking; } void PlainTextEditor::setSpellCheckingSupport(bool check) { if (check) { d->supportFeatures |= SpellChecking; } else { d->supportFeatures = (d->supportFeatures & ~SpellChecking); } } void PlainTextEditor::setWebShortcutSupport(bool b) { if (b) { d->supportFeatures |= AllowWebShortcut; } else { d->supportFeatures = (d->supportFeatures & ~AllowWebShortcut); } } bool PlainTextEditor::webShortcutSupport() const { return d->supportFeatures & AllowWebShortcut; } void PlainTextEditor::setReadOnly(bool readOnly) { if (!readOnly && hasFocus() && d->checkSpellingEnabled && !d->richTextDecorator) { createHighlighter(); } if (readOnly == isReadOnly()) { return; } if (readOnly) { clearDecorator(); d->customPalette = testAttribute(Qt::WA_SetPalette); QPalette p = palette(); QColor color = p.color(QPalette::Disabled, QPalette::Window); p.setColor(QPalette::Base, color); p.setColor(QPalette::Window, color); setPalette(p); } else { if (d->customPalette && testAttribute(Qt::WA_SetPalette)) { QPalette p = palette(); QColor color = p.color(QPalette::Normal, QPalette::Base); p.setColor(QPalette::Base, color); p.setColor(QPalette::Window, color); setPalette(p); } else { setPalette(QPalette()); } } QPlainTextEdit::setReadOnly(readOnly); } void PlainTextEditor::slotCheckSpelling() { if (document()->isEmpty()) { slotDisplayMessageIndicator(i18n("Nothing to spell check.")); return; } Sonnet::BackgroundChecker *backgroundSpellCheck = new Sonnet::BackgroundChecker; if (backgroundSpellCheck->speller().availableBackends().isEmpty()) { slotDisplayMessageIndicator(i18n("No backend available for spell checking.")); delete backgroundSpellCheck; return; } if (!d->spellCheckingLanguage.isEmpty()) { backgroundSpellCheck->changeLanguage(d->spellCheckingLanguage); } if (!d->ignoreSpellCheckingWords.isEmpty()) { for (const QString &word : qAsConst(d->ignoreSpellCheckingWords)) { backgroundSpellCheck->speller().addToSession(word); } } Sonnet::Dialog *spellDialog = new Sonnet::Dialog(backgroundSpellCheck, nullptr); backgroundSpellCheck->setParent(spellDialog); spellDialog->setAttribute(Qt::WA_DeleteOnClose, true); connect(spellDialog, &Sonnet::Dialog::replace, this, &PlainTextEditor::slotSpellCheckerCorrected); connect(spellDialog, &Sonnet::Dialog::misspelling, this, &PlainTextEditor::slotSpellCheckerMisspelling); connect(spellDialog, &Sonnet::Dialog::autoCorrect, this, &PlainTextEditor::slotSpellCheckerAutoCorrect); connect(spellDialog, QOverload::of(&Sonnet::Dialog::done), this, &PlainTextEditor::slotSpellCheckerFinished); connect(spellDialog, &Sonnet::Dialog::cancel, this, &PlainTextEditor::slotSpellCheckerCanceled); connect(spellDialog, &Sonnet::Dialog::spellCheckStatus, this, &PlainTextEditor::spellCheckStatus); connect(spellDialog, &Sonnet::Dialog::languageChanged, this, &PlainTextEditor::languageChanged); d->originalDoc = QTextDocumentFragment(document()); spellDialog->setBuffer(toPlainText()); spellDialog->show(); } void PlainTextEditor::slotSpellCheckerCanceled() { QTextDocument *doc = document(); doc->clear(); QTextCursor cursor(doc); cursor.insertFragment(d->originalDoc); slotSpellCheckerFinished(); } void PlainTextEditor::slotSpellCheckerAutoCorrect(const QString ¤tWord, const QString &autoCorrectWord) { Q_EMIT spellCheckerAutoCorrect(currentWord, autoCorrectWord); } void PlainTextEditor::slotSpellCheckerMisspelling(const QString &text, int pos) { highlightWord(text.length(), pos); } void PlainTextEditor::slotSpellCheckerCorrected(const QString &oldWord, int pos, const QString &newWord) { if (oldWord != newWord) { QTextCursor cursor(document()); cursor.setPosition(pos); cursor.setPosition(pos + oldWord.length(), QTextCursor::KeepAnchor); cursor.insertText(newWord); } } void PlainTextEditor::slotSpellCheckerFinished() { QTextCursor cursor(document()); cursor.clearSelection(); setTextCursor(cursor); } void PlainTextEditor::highlightWord(int length, int pos) { QTextCursor cursor(document()); cursor.setPosition(pos); cursor.setPosition(pos + length, QTextCursor::KeepAnchor); setTextCursor(cursor); ensureCursorVisible(); } static void deleteWord(QTextCursor cursor, QTextCursor::MoveOperation op) { cursor.clearSelection(); cursor.movePosition(op, QTextCursor::KeepAnchor); cursor.removeSelectedText(); } void PlainTextEditor::deleteWordBack() { deleteWord(textCursor(), QTextCursor::PreviousWord); } void PlainTextEditor::deleteWordForward() { deleteWord(textCursor(), QTextCursor::WordRight); } bool PlainTextEditor::event(QEvent *ev) { if (ev->type() == QEvent::ShortcutOverride) { QKeyEvent *e = static_cast(ev); if (overrideShortcut(e)) { e->accept(); return true; } } return QPlainTextEdit::event(ev); } bool PlainTextEditor::overrideShortcut(QKeyEvent *event) { const int key = event->key() | event->modifiers(); if (KStandardShortcut::copy().contains(key)) { return true; } else if (KStandardShortcut::paste().contains(key)) { return true; } else if (KStandardShortcut::cut().contains(key)) { return true; } else if (KStandardShortcut::undo().contains(key)) { return true; } else if (KStandardShortcut::redo().contains(key)) { return true; } else if (KStandardShortcut::deleteWordBack().contains(key)) { return true; } else if (KStandardShortcut::deleteWordForward().contains(key)) { return true; } else if (KStandardShortcut::backwardWord().contains(key)) { return true; } else if (KStandardShortcut::forwardWord().contains(key)) { return true; } else if (KStandardShortcut::next().contains(key)) { return true; } else if (KStandardShortcut::prior().contains(key)) { return true; } else if (KStandardShortcut::begin().contains(key)) { return true; } else if (KStandardShortcut::end().contains(key)) { return true; } else if (KStandardShortcut::beginningOfLine().contains(key)) { return true; } else if (KStandardShortcut::endOfLine().contains(key)) { return true; } else if (KStandardShortcut::pasteSelection().contains(key)) { return true; } else if (searchSupport() && KStandardShortcut::find().contains(key)) { return true; } else if (searchSupport() && KStandardShortcut::replace().contains(key)) { return true; } else if (searchSupport() && KStandardShortcut::findNext().contains(key)) { return true; } else if (event->matches(QKeySequence::SelectAll)) { // currently missing in QTextEdit return true; } else if (event == QKeySequence::DeleteEndOfLine) { return true; } return false; } bool PlainTextEditor::handleShortcut(QKeyEvent *event) { const int key = event->key() | event->modifiers(); if (KStandardShortcut::copy().contains(key)) { copy(); return true; } else if (KStandardShortcut::paste().contains(key)) { paste(); return true; } else if (KStandardShortcut::cut().contains(key)) { cut(); return true; } else if (KStandardShortcut::undo().contains(key)) { if (!isReadOnly()) { undo(); } return true; } else if (KStandardShortcut::redo().contains(key)) { if (!isReadOnly()) { redo(); } return true; } else if (KStandardShortcut::deleteWordBack().contains(key)) { if (!isReadOnly()) { deleteWordBack(); } return true; } else if (KStandardShortcut::deleteWordForward().contains(key)) { if (!isReadOnly()) { deleteWordForward(); } return true; } else if (KStandardShortcut::backwardWord().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::PreviousWord); setTextCursor(cursor); return true; } else if (KStandardShortcut::forwardWord().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::NextWord); setTextCursor(cursor); return true; } else if (KStandardShortcut::next().contains(key)) { QTextCursor cursor = textCursor(); bool moved = false; qreal lastY = cursorRect(cursor).bottom(); qreal distance = 0; do { qreal y = cursorRect(cursor).bottom(); distance += qAbs(y - lastY); lastY = y; moved = cursor.movePosition(QTextCursor::Down); } while (moved && distance < viewport()->height()); if (moved) { cursor.movePosition(QTextCursor::Up); verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd); } setTextCursor(cursor); return true; } else if (KStandardShortcut::prior().contains(key)) { QTextCursor cursor = textCursor(); bool moved = false; qreal lastY = cursorRect(cursor).bottom(); qreal distance = 0; do { qreal y = cursorRect(cursor).bottom(); distance += qAbs(y - lastY); lastY = y; moved = cursor.movePosition(QTextCursor::Up); } while (moved && distance < viewport()->height()); if (moved) { cursor.movePosition(QTextCursor::Down); verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub); } setTextCursor(cursor); return true; } else if (KStandardShortcut::begin().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::Start); setTextCursor(cursor); return true; } else if (KStandardShortcut::end().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::End); setTextCursor(cursor); return true; } else if (KStandardShortcut::beginningOfLine().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::StartOfLine); setTextCursor(cursor); return true; } else if (KStandardShortcut::endOfLine().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::EndOfLine); setTextCursor(cursor); return true; } else if (searchSupport() && KStandardShortcut::find().contains(key)) { Q_EMIT findText(); return true; } else if (searchSupport() && KStandardShortcut::replace().contains(key)) { if (!isReadOnly()) { Q_EMIT replaceText(); } return true; } else if (KStandardShortcut::pasteSelection().contains(key)) { QString text = QApplication::clipboard()->text(QClipboard::Selection); if (!text.isEmpty()) { insertPlainText(text); // TODO: check if this is html? (MiB) } return true; } else if (event == QKeySequence::DeleteEndOfLine) { deleteEndOfLine(); return true; } return false; } void PlainTextEditor::deleteEndOfLine() { QTextCursor cursor = textCursor(); QTextBlock block = cursor.block(); if (cursor.position() == block.position() + block.length() - 2) { cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); } else { cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); } cursor.removeSelectedText(); setTextCursor(cursor); } void PlainTextEditor::moveLineUpDown(bool moveUp) { QTextCursor cursor = textCursor(); QTextCursor move = cursor; move.beginEditBlock(); bool hasSelection = cursor.hasSelection(); if (hasSelection) { move.setPosition(cursor.selectionStart()); move.movePosition(QTextCursor::StartOfBlock); move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor); move.movePosition(move.atBlockStart() ? QTextCursor::Left : QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); } else { move.movePosition(QTextCursor::StartOfBlock); move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); } QString text = move.selectedText(); move.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); move.removeSelectedText(); if (moveUp) { move.movePosition(QTextCursor::PreviousBlock); move.insertBlock(); move.movePosition(QTextCursor::Left); } else { move.movePosition(QTextCursor::EndOfBlock); if (move.atBlockStart()) { // empty block move.movePosition(QTextCursor::NextBlock); move.insertBlock(); move.movePosition(QTextCursor::Left); } else { move.insertBlock(); } } int start = move.position(); move.clearSelection(); move.insertText(text); int end = move.position(); if (hasSelection) { move.setPosition(end); move.setPosition(start, QTextCursor::KeepAnchor); } else { move.setPosition(start); } move.endEditBlock(); setTextCursor(move); } void PlainTextEditor::wheelEvent(QWheelEvent *event) { if (QApplication::keyboardModifiers() & Qt::ControlModifier) { if (event->delta() > 0) { zoomIn(); } else if (event->delta() < 0) { zoomOut(); } event->accept(); return; } QPlainTextEdit::wheelEvent(event); } void PlainTextEditor::keyPressEvent(QKeyEvent *event) { const bool isControlClicked = event->modifiers() & Qt::ControlModifier; const bool isShiftClicked = event->modifiers() & Qt::ShiftModifier; if (handleShortcut(event)) { event->accept(); } else if (event->key() == Qt::Key_Up && isControlClicked && isShiftClicked) { moveLineUpDown(true); event->accept(); } else if (event->key() == Qt::Key_Down && isControlClicked && isShiftClicked) { moveLineUpDown(false); event->accept(); } else { QPlainTextEdit::keyPressEvent(event); } } bool PlainTextEditor::activateLanguageMenu() const { return d->activateLanguageMenu; } void PlainTextEditor::setActivateLanguageMenu(bool activate) { d->activateLanguageMenu = activate; } Sonnet::Highlighter *PlainTextEditor::highlighter() const { if (d->richTextDecorator) { return d->richTextDecorator->highlighter(); } else { return nullptr; } } Sonnet::SpellCheckDecorator *PlainTextEditor::createSpellCheckDecorator() { return new Sonnet::SpellCheckDecorator(this); } void PlainTextEditor::addIgnoreWordsToHighLighter() { if (d->ignoreSpellCheckingWords.isEmpty()) { return; } if (d->richTextDecorator) { Sonnet::Highlighter *_highlighter = d->richTextDecorator->highlighter(); for (const QString &word : qAsConst(d->ignoreSpellCheckingWords)) { _highlighter->ignoreWord(word); } } } void PlainTextEditor::setHighlighter(Sonnet::Highlighter *_highLighter) { Sonnet::SpellCheckDecorator *decorator = createSpellCheckDecorator(); delete decorator->highlighter(); decorator->setHighlighter(_highLighter); //KTextEdit used to take ownership of the highlighter, Sonnet::SpellCheckDecorator does not. //so we reparent the highlighter so it will be deleted when the decorator is destroyed _highLighter->setParent(decorator); d->richTextDecorator = decorator; addIgnoreWordsToHighLighter(); } void PlainTextEditor::focusInEvent(QFocusEvent *event) { if (checkSpellingEnabled() && !isReadOnly() && !d->richTextDecorator && spellCheckingSupport()) { createHighlighter(); } QPlainTextEdit::focusInEvent(event); } bool PlainTextEditor::checkSpellingEnabled() const { return d->checkSpellingEnabled; } void PlainTextEditor::setCheckSpellingEnabled(bool check) { if (check == d->checkSpellingEnabled) { return; } d->checkSpellingEnabled = check; Q_EMIT checkSpellingChanged(check); // From the above statement we know that if we're turning checking // on that we need to create a new highlighter and if we're turning it // off we should remove the old one. if (check) { if (hasFocus()) { if (!d->richTextDecorator) { createHighlighter(); } if (!d->spellCheckingLanguage.isEmpty()) { setSpellCheckingLanguage(spellCheckingLanguage()); } } } else { clearDecorator(); } updateHighLighter(); } void PlainTextEditor::updateHighLighter() { } void PlainTextEditor::clearDecorator() { delete d->richTextDecorator; d->richTextDecorator = nullptr; } void PlainTextEditor::createHighlighter() { Sonnet::Highlighter *highlighter = new Sonnet::Highlighter(this); highlighter->setCurrentLanguage(spellCheckingLanguage()); setHighlighter(highlighter); } void PlainTextEditor::setSpellCheckingConfigFileName(const QString &_fileName) { d->spellCheckingConfigFileName = _fileName; KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName); if (config->hasGroup("Spelling")) { KConfigGroup group(config, "Spelling"); d->checkSpellingEnabled = group.readEntry("checkerEnabledByDefault", false); d->spellCheckingLanguage = group.readEntry("Language", QString()); } setCheckSpellingEnabled(checkSpellingEnabled()); if (!d->spellCheckingLanguage.isEmpty() && highlighter()) { highlighter()->setCurrentLanguage(d->spellCheckingLanguage); highlighter()->rehighlight(); } } QString PlainTextEditor::spellCheckingConfigFileName() const { return d->spellCheckingConfigFileName; } void PlainTextEditor::slotLanguageSelected() { QAction *languageAction = static_cast(QObject::sender()); setSpellCheckingLanguage(languageAction->data().toString()); } const QString &PlainTextEditor::spellCheckingLanguage() const { return d->spellCheckingLanguage; } void PlainTextEditor::setSpellCheckingLanguage(const QString &_language) { if (highlighter()) { highlighter()->setCurrentLanguage(_language); highlighter()->rehighlight(); } if (_language != d->spellCheckingLanguage) { d->spellCheckingLanguage = _language; KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName); KConfigGroup group(config, "Spelling"); group.writeEntry("Language", d->spellCheckingLanguage); setCheckSpellingEnabled(checkSpellingEnabled()); Q_EMIT languageChanged(_language); } } void PlainTextEditor::slotToggleAutoSpellCheck() { setCheckSpellingEnabled(!checkSpellingEnabled()); KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName); KConfigGroup group(config, "Spelling"); group.writeEntry("checkerEnabledByDefault", d->checkSpellingEnabled); } void PlainTextEditor::slotZoomReset() { QFont f = font(); if (d->mInitialFontSize != f.pointSize()) { f.setPointSize(d->mInitialFontSize); setFont(f); } } diff --git a/src/texteditor/richtexteditor/richtexteditor.cpp b/src/texteditor/richtexteditor/richtexteditor.cpp index cdf57a1..267489c 100644 --- a/src/texteditor/richtexteditor/richtexteditor.cpp +++ b/src/texteditor/richtexteditor/richtexteditor.cpp @@ -1,996 +1,996 @@ /* Copyright (C) 2013-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "richtexteditor.h" #include "kpimtextedit_debug.h" #include "texteditor/commonwidget/textmessageindicator.h" #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 #include #include #include using namespace KPIMTextEdit; class Q_DECL_HIDDEN RichTextEditor::RichTextEditorPrivate { public: RichTextEditorPrivate(RichTextEditor *qq) : q(qq) , textIndicator(new KPIMTextEdit::TextMessageIndicator(q)) , webshortcutMenuManager(new KIO::KUriFilterSearchProviderActions(q)) { KConfig sonnetKConfig(QStringLiteral("sonnetrc")); KConfigGroup group(&sonnetKConfig, "Spelling"); checkSpellingEnabled = group.readEntry("checkerEnabledByDefault", false); supportFeatures |= RichTextEditor::Search; supportFeatures |= RichTextEditor::SpellChecking; supportFeatures |= RichTextEditor::TextToSpeech; supportFeatures |= RichTextEditor::AllowTab; supportFeatures |= RichTextEditor::AllowWebShortcut; } ~RichTextEditorPrivate() { delete richTextDecorator; delete speller; } QStringList ignoreSpellCheckingWords; RichTextEditor *q = nullptr; KPIMTextEdit::TextMessageIndicator *textIndicator = nullptr; QString spellCheckingConfigFileName; QString spellCheckingLanguage; QTextDocumentFragment originalDoc; Sonnet::SpellCheckDecorator *richTextDecorator = nullptr; Sonnet::Speller *speller = nullptr; KIO::KUriFilterSearchProviderActions *webshortcutMenuManager = nullptr; RichTextEditor::SupportFeatures supportFeatures; int mInitialFontSize; bool customPalette = false; bool checkSpellingEnabled = false; bool activateLanguageMenu = true; bool showAutoCorrectionButton = false; }; RichTextEditor::RichTextEditor(QWidget *parent) : QTextEdit(parent) , d(new RichTextEditorPrivate(this)) { setAcceptRichText(true); KCursor::setAutoHideCursor(this, true, false); setSpellCheckingConfigFileName(QString()); d->mInitialFontSize = font().pointSize(); } RichTextEditor::~RichTextEditor() { delete d; } void RichTextEditor::setDefaultFontSize(int val) { d->mInitialFontSize = val; slotZoomReset(); } void RichTextEditor::slotDisplayMessageIndicator(const QString &message) { d->textIndicator->display(message); } Sonnet::Highlighter *RichTextEditor::highlighter() const { if (d->richTextDecorator) { return d->richTextDecorator->highlighter(); } else { return nullptr; } } bool RichTextEditor::activateLanguageMenu() const { return d->activateLanguageMenu; } void RichTextEditor::setActivateLanguageMenu(bool activate) { d->activateLanguageMenu = activate; } void RichTextEditor::contextMenuEvent(QContextMenuEvent *event) { QMenu *popup = mousePopupMenu(event->pos()); if (popup) { popup->exec(event->globalPos()); delete popup; } } QMenu *RichTextEditor::mousePopupMenu(QPoint pos) { QMenu *popup = createStandardContextMenu(); if (popup) { const bool emptyDocument = document()->isEmpty(); if (!isReadOnly()) { QList actionList = popup->actions(); enum { UndoAct, RedoAct, CutAct, CopyAct, PasteAct, ClearAct, SelectAllAct, NCountActs }; QAction *separatorAction = nullptr; const int idx = actionList.indexOf(actionList[SelectAllAct]) + 1; if (idx < actionList.count()) { separatorAction = actionList.at(idx); } if (separatorAction) { QAction *clearAllAction = KStandardAction::clear(this, &RichTextEditor::slotUndoableClear, popup); if (emptyDocument) { clearAllAction->setEnabled(false); } popup->insertAction(separatorAction, clearAllAction); } } KIconTheme::assignIconsToContextMenu(isReadOnly() ? KIconTheme::ReadOnlyText : KIconTheme::TextEditor, popup->actions()); if (searchSupport()) { popup->addSeparator(); - QAction *findAct = popup->addAction(KStandardGuiItem::find().icon(), KStandardGuiItem::find().text(), this, SIGNAL(findText()), Qt::Key_F + Qt::CTRL); + QAction *findAct = popup->addAction(KStandardGuiItem::find().icon(), KStandardGuiItem::find().text(), this, &RichTextEditor::findText, Qt::Key_F + Qt::CTRL); if (emptyDocument) { findAct->setEnabled(false); } popup->addSeparator(); if (!isReadOnly()) { - QAction *act = popup->addAction(i18n("Replace..."), this, SIGNAL(replaceText()), Qt::Key_R + Qt::CTRL); + QAction *act = popup->addAction(i18n("Replace..."), this, &RichTextEditor::replaceText, Qt::Key_R + Qt::CTRL); if (emptyDocument) { act->setEnabled(false); } popup->addSeparator(); } } else { popup->addSeparator(); } if (!isReadOnly() && spellCheckingSupport()) { if (!d->speller) { d->speller = new Sonnet::Speller(); } if (!d->speller->availableBackends().isEmpty()) { QAction *spellCheckAction = popup->addAction(QIcon::fromTheme(QStringLiteral("tools-check-spelling")), i18n("Check Spelling..."), this, &RichTextEditor::slotCheckSpelling); if (emptyDocument) { spellCheckAction->setEnabled(false); } popup->addSeparator(); QAction *autoSpellCheckAction = popup->addAction(i18n("Auto Spell Check"), this, &RichTextEditor::slotToggleAutoSpellCheck); autoSpellCheckAction->setCheckable(true); autoSpellCheckAction->setChecked(checkSpellingEnabled()); popup->addAction(autoSpellCheckAction); if (checkSpellingEnabled() && d->activateLanguageMenu) { QMenu *languagesMenu = new QMenu(i18n("Spell Checking Language"), popup); QActionGroup *languagesGroup = new QActionGroup(languagesMenu); languagesGroup->setExclusive(true); QString defaultSpellcheckingLanguage = spellCheckingLanguage(); if (defaultSpellcheckingLanguage.isEmpty()) { defaultSpellcheckingLanguage = d->speller->defaultLanguage(); } QMapIterator i(d->speller->availableDictionaries()); while (i.hasNext()) { i.next(); QAction *languageAction = languagesMenu->addAction(i.key()); languageAction->setCheckable(true); languageAction->setChecked(defaultSpellcheckingLanguage == i.value()); languageAction->setData(i.value()); languageAction->setActionGroup(languagesGroup); connect(languageAction, &QAction::triggered, this, &RichTextEditor::slotLanguageSelected); } popup->addMenu(languagesMenu); } popup->addSeparator(); } } if (allowTabSupport() && !isReadOnly()) { QAction *allowTabAction = popup->addAction(i18n("Allow Tabulations")); allowTabAction->setCheckable(true); allowTabAction->setChecked(!tabChangesFocus()); connect(allowTabAction, &QAction::triggered, this, &RichTextEditor::slotAllowTab); } if (KPIMTextEdit::TextToSpeech::self()->isReady()) { if (!emptyDocument) { QAction *speakAction = popup->addAction(i18n("Speak Text")); speakAction->setIcon(QIcon::fromTheme(QStringLiteral("preferences-desktop-text-to-speech"))); connect(speakAction, &QAction::triggered, this, &RichTextEditor::slotSpeakText); } } if (webShortcutSupport() && textCursor().hasSelection()) { popup->addSeparator(); const QString selectedText = textCursor().selectedText(); d->webshortcutMenuManager->setSelectedText(selectedText); d->webshortcutMenuManager->addWebShortcutsToMenu(popup); } addExtraMenuEntry(popup, pos); return popup; } return nullptr; } void RichTextEditor::slotSpeakText() { QString text; if (textCursor().hasSelection()) { text = textCursor().selectedText(); } else { text = toPlainText(); } Q_EMIT say(text); } void RichTextEditor::setWebShortcutSupport(bool b) { if (b) { d->supportFeatures |= AllowWebShortcut; } else { d->supportFeatures = (d->supportFeatures & ~AllowWebShortcut); } } bool RichTextEditor::webShortcutSupport() const { return d->supportFeatures & AllowWebShortcut; } void RichTextEditor::addIgnoreWords(const QStringList &lst) { d->ignoreSpellCheckingWords = lst; addIgnoreWordsToHighLighter(); } void RichTextEditor::forceAutoCorrection(bool selectedText) { Q_UNUSED(selectedText); //Nothing here } void RichTextEditor::setSearchSupport(bool b) { if (b) { d->supportFeatures |= Search; } else { d->supportFeatures = (d->supportFeatures & ~Search); } } bool RichTextEditor::searchSupport() const { return d->supportFeatures & Search; } void RichTextEditor::setAllowTabSupport(bool b) { if (b) { d->supportFeatures |= AllowTab; } else { d->supportFeatures = (d->supportFeatures & ~AllowTab); } } bool RichTextEditor::allowTabSupport() const { return d->supportFeatures & AllowTab; } void RichTextEditor::setShowAutoCorrectButton(bool b) { d->showAutoCorrectionButton = b; } bool RichTextEditor::showAutoCorrectButton() const { return d->showAutoCorrectionButton; } bool RichTextEditor::spellCheckingSupport() const { return d->supportFeatures & SpellChecking; } void RichTextEditor::setSpellCheckingSupport(bool check) { if (check) { d->supportFeatures |= SpellChecking; } else { d->supportFeatures = (d->supportFeatures & ~SpellChecking); } } void RichTextEditor::setTextToSpeechSupport(bool b) { if (b) { d->supportFeatures |= TextToSpeech; } else { d->supportFeatures = (d->supportFeatures & ~TextToSpeech); } } bool RichTextEditor::textToSpeechSupport() const { return d->supportFeatures & TextToSpeech; } void RichTextEditor::slotAllowTab() { setTabChangesFocus(!tabChangesFocus()); } void RichTextEditor::addExtraMenuEntry(QMenu *menu, QPoint pos) { Q_UNUSED(menu); Q_UNUSED(pos); } void RichTextEditor::slotUndoableClear() { QTextCursor cursor = textCursor(); cursor.beginEditBlock(); cursor.movePosition(QTextCursor::Start); cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor); cursor.removeSelectedText(); cursor.endEditBlock(); } void RichTextEditor::setReadOnly(bool readOnly) { if (!readOnly && hasFocus() && checkSpellingEnabled() && !d->richTextDecorator) { createHighlighter(); } if (readOnly == isReadOnly()) { return; } if (readOnly) { clearDecorator(); d->customPalette = testAttribute(Qt::WA_SetPalette); QPalette p = palette(); QColor color = p.color(QPalette::Disabled, QPalette::Window); p.setColor(QPalette::Base, color); p.setColor(QPalette::Window, color); setPalette(p); } else { if (d->customPalette && testAttribute(Qt::WA_SetPalette)) { QPalette p = palette(); QColor color = p.color(QPalette::Normal, QPalette::Base); p.setColor(QPalette::Base, color); p.setColor(QPalette::Window, color); setPalette(p); } else { setPalette(QPalette()); } } QTextEdit::setReadOnly(readOnly); } void RichTextEditor::checkSpelling(bool force) { if (document()->isEmpty()) { slotDisplayMessageIndicator(i18n("Nothing to spell check.")); if (force) { Q_EMIT spellCheckingFinished(); } return; } Sonnet::BackgroundChecker *backgroundSpellCheck = new Sonnet::BackgroundChecker; if (backgroundSpellCheck->speller().availableBackends().isEmpty()) { if (force) { if (KMessageBox::Yes == KMessageBox::questionYesNo(this, i18n("No backend available for spell checking. Do you want to send the email anyways?"))) { Q_EMIT spellCheckingFinished(); } } else { slotDisplayMessageIndicator(i18n("No backend available for spell checking.")); } delete backgroundSpellCheck; return; } if (!d->spellCheckingLanguage.isEmpty()) { backgroundSpellCheck->changeLanguage(d->spellCheckingLanguage); } if (!d->ignoreSpellCheckingWords.isEmpty()) { for (const QString &word : qAsConst(d->ignoreSpellCheckingWords)) { backgroundSpellCheck->speller().addToSession(word); } } Sonnet::Dialog *spellDialog = new Sonnet::Dialog(backgroundSpellCheck, force ? this : nullptr); QDialogButtonBox *buttonBox = spellDialog->findChild(); if (buttonBox) { QPushButton *skipButton = new QPushButton(i18n("Skip")); buttonBox->addButton(skipButton, QDialogButtonBox::ActionRole); connect(skipButton, &QPushButton::clicked, spellDialog, &Sonnet::Dialog::close); if (force) { connect(skipButton, &QPushButton::clicked, this, &RichTextEditor::spellCheckingFinished); } } else { qCWarning(KPIMTEXTEDIT_LOG) << " Impossible to find qdialogbuttonbox"; } backgroundSpellCheck->setParent(spellDialog); spellDialog->setAttribute(Qt::WA_DeleteOnClose, true); spellDialog->activeAutoCorrect(d->showAutoCorrectionButton); connect(spellDialog, &Sonnet::Dialog::replace, this, &RichTextEditor::slotSpellCheckerCorrected); connect(spellDialog, &Sonnet::Dialog::misspelling, this, &RichTextEditor::slotSpellCheckerMisspelling); connect(spellDialog, &Sonnet::Dialog::autoCorrect, this, &RichTextEditor::slotSpellCheckerAutoCorrect); connect(spellDialog, QOverload::of(&Sonnet::Dialog::done), this, &RichTextEditor::slotSpellCheckerFinished); connect(spellDialog, &Sonnet::Dialog::cancel, this, &RichTextEditor::slotSpellCheckerCanceled); connect(spellDialog, &Sonnet::Dialog::spellCheckStatus, this, &RichTextEditor::spellCheckStatus); connect(spellDialog, &Sonnet::Dialog::languageChanged, this, &RichTextEditor::languageChanged); if (force) { connect(spellDialog, SIGNAL(done(QString)), this, SIGNAL(spellCheckingFinished())); //connect(spellDialog, &Sonnet::Dialog::done, this, &RichTextEditor::spellCheckingFinished); connect(spellDialog, &Sonnet::Dialog::cancel, this, &RichTextEditor::spellCheckingCanceled); } d->originalDoc = QTextDocumentFragment(document()); spellDialog->setBuffer(toPlainText()); spellDialog->show(); } void RichTextEditor::slotCheckSpelling() { checkSpelling(false); } void RichTextEditor::forceSpellChecking() { checkSpelling(true); } void RichTextEditor::slotSpellCheckerCanceled() { QTextDocument *doc = document(); doc->clear(); QTextCursor cursor(doc); cursor.insertFragment(d->originalDoc); slotSpellCheckerFinished(); } void RichTextEditor::slotSpellCheckerAutoCorrect(const QString ¤tWord, const QString &autoCorrectWord) { Q_EMIT spellCheckerAutoCorrect(currentWord, autoCorrectWord); } void RichTextEditor::slotSpellCheckerMisspelling(const QString &text, int pos) { highlightWord(text.length(), pos); } void RichTextEditor::slotSpellCheckerCorrected(const QString &oldWord, int pos, const QString &newWord) { if (oldWord != newWord) { QTextCursor cursor(document()); cursor.setPosition(pos); cursor.setPosition(pos + oldWord.length(), QTextCursor::KeepAnchor); cursor.insertText(newWord); } } void RichTextEditor::slotSpellCheckerFinished() { QTextCursor cursor(document()); cursor.clearSelection(); setTextCursor(cursor); if (highlighter()) { highlighter()->rehighlight(); } } void RichTextEditor::highlightWord(int length, int pos) { QTextCursor cursor(document()); cursor.setPosition(pos); cursor.setPosition(pos + length, QTextCursor::KeepAnchor); setTextCursor(cursor); ensureCursorVisible(); } void RichTextEditor::createHighlighter() { Sonnet::Highlighter *highlighter = new Sonnet::Highlighter(this); highlighter->setCurrentLanguage(spellCheckingLanguage()); setHighlighter(highlighter); } Sonnet::SpellCheckDecorator *RichTextEditor::createSpellCheckDecorator() { return new Sonnet::SpellCheckDecorator(this); } void RichTextEditor::addIgnoreWordsToHighLighter() { if (d->ignoreSpellCheckingWords.isEmpty()) { return; } if (d->richTextDecorator) { Sonnet::Highlighter *_highlighter = d->richTextDecorator->highlighter(); for (const QString &word : qAsConst(d->ignoreSpellCheckingWords)) { _highlighter->ignoreWord(word); } } } void RichTextEditor::setHighlighter(Sonnet::Highlighter *_highLighter) { Sonnet::SpellCheckDecorator *decorator = createSpellCheckDecorator(); delete decorator->highlighter(); decorator->setHighlighter(_highLighter); //KTextEdit used to take ownership of the highlighter, Sonnet::SpellCheckDecorator does not. //so we reparent the highlighter so it will be deleted when the decorator is destroyed _highLighter->setParent(decorator); d->richTextDecorator = decorator; addIgnoreWordsToHighLighter(); } void RichTextEditor::focusInEvent(QFocusEvent *event) { if (d->checkSpellingEnabled && !isReadOnly() && !d->richTextDecorator && spellCheckingSupport()) { createHighlighter(); } QTextEdit::focusInEvent(event); } void RichTextEditor::setSpellCheckingConfigFileName(const QString &_fileName) { d->spellCheckingConfigFileName = _fileName; KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName); if (config->hasGroup("Spelling")) { KConfigGroup group(config, "Spelling"); d->checkSpellingEnabled = group.readEntry("checkerEnabledByDefault", false); d->spellCheckingLanguage = group.readEntry("Language", QString()); } setCheckSpellingEnabled(checkSpellingEnabled()); if (!d->spellCheckingLanguage.isEmpty() && highlighter()) { highlighter()->setCurrentLanguage(d->spellCheckingLanguage); highlighter()->rehighlight(); } } QString RichTextEditor::spellCheckingConfigFileName() const { return d->spellCheckingConfigFileName; } bool RichTextEditor::checkSpellingEnabled() const { return d->checkSpellingEnabled; } void RichTextEditor::setCheckSpellingEnabled(bool check) { if (check == d->checkSpellingEnabled) { return; } d->checkSpellingEnabled = check; Q_EMIT checkSpellingChanged(check); // From the above statement we know that if we're turning checking // on that we need to create a new highlighter and if we're turning it // off we should remove the old one. if (check) { if (hasFocus()) { if (!d->richTextDecorator) { createHighlighter(); } if (!d->spellCheckingLanguage.isEmpty()) { setSpellCheckingLanguage(spellCheckingLanguage()); } } } else { clearDecorator(); } updateHighLighter(); } void RichTextEditor::updateHighLighter() { } void RichTextEditor::clearDecorator() { delete d->richTextDecorator; d->richTextDecorator = nullptr; } const QString &RichTextEditor::spellCheckingLanguage() const { return d->spellCheckingLanguage; } void RichTextEditor::setSpellCheckingLanguage(const QString &_language) { if (highlighter()) { highlighter()->setCurrentLanguage(_language); } if (_language != d->spellCheckingLanguage) { d->spellCheckingLanguage = _language; KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName); KConfigGroup group(config, "Spelling"); group.writeEntry("Language", d->spellCheckingLanguage); Q_EMIT languageChanged(_language); } } void RichTextEditor::slotToggleAutoSpellCheck() { setCheckSpellingEnabled(!checkSpellingEnabled()); KSharedConfig::Ptr config = KSharedConfig::openConfig(d->spellCheckingConfigFileName); KConfigGroup group(config, "Spelling"); group.writeEntry("checkerEnabledByDefault", d->checkSpellingEnabled); } void RichTextEditor::slotLanguageSelected() { QAction *languageAction = static_cast(QObject::sender()); setSpellCheckingLanguage(languageAction->data().toString()); } static void deleteWord(QTextCursor cursor, QTextCursor::MoveOperation op) { cursor.clearSelection(); cursor.movePosition(op, QTextCursor::KeepAnchor); cursor.removeSelectedText(); } void RichTextEditor::deleteWordBack() { deleteWord(textCursor(), QTextCursor::PreviousWord); } void RichTextEditor::deleteWordForward() { deleteWord(textCursor(), QTextCursor::WordRight); } bool RichTextEditor::event(QEvent *ev) { if (ev->type() == QEvent::ShortcutOverride) { QKeyEvent *e = static_cast(ev); if (overrideShortcut(e)) { e->accept(); return true; } } return QTextEdit::event(ev); } void RichTextEditor::wheelEvent(QWheelEvent *event) { if (QApplication::keyboardModifiers() & Qt::ControlModifier) { if (event->delta() > 0) { zoomIn(); } else if (event->delta() < 0) { zoomOut(); } event->accept(); return; } QTextEdit::wheelEvent(event); } bool RichTextEditor::handleShortcut(QKeyEvent *event) { const int key = event->key() | event->modifiers(); if (KStandardShortcut::copy().contains(key)) { copy(); return true; } else if (KStandardShortcut::paste().contains(key)) { paste(); return true; } else if (KStandardShortcut::cut().contains(key)) { cut(); return true; } else if (KStandardShortcut::undo().contains(key)) { if (!isReadOnly()) { undo(); } return true; } else if (KStandardShortcut::redo().contains(key)) { if (!isReadOnly()) { redo(); } return true; } else if (KStandardShortcut::deleteWordBack().contains(key)) { if (!isReadOnly()) { deleteWordBack(); } return true; } else if (KStandardShortcut::deleteWordForward().contains(key)) { if (!isReadOnly()) { deleteWordForward(); } return true; } else if (KStandardShortcut::backwardWord().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::PreviousWord); setTextCursor(cursor); return true; } else if (KStandardShortcut::forwardWord().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::NextWord); setTextCursor(cursor); return true; } else if (KStandardShortcut::next().contains(key)) { QTextCursor cursor = textCursor(); bool moved = false; qreal lastY = cursorRect(cursor).bottom(); qreal distance = 0; do { qreal y = cursorRect(cursor).bottom(); distance += qAbs(y - lastY); lastY = y; moved = cursor.movePosition(QTextCursor::Down); } while (moved && distance < viewport()->height()); if (moved) { cursor.movePosition(QTextCursor::Up); verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepAdd); } setTextCursor(cursor); return true; } else if (KStandardShortcut::prior().contains(key)) { QTextCursor cursor = textCursor(); bool moved = false; qreal lastY = cursorRect(cursor).bottom(); qreal distance = 0; do { qreal y = cursorRect(cursor).bottom(); distance += qAbs(y - lastY); lastY = y; moved = cursor.movePosition(QTextCursor::Up); } while (moved && distance < viewport()->height()); if (moved) { cursor.movePosition(QTextCursor::Down); verticalScrollBar()->triggerAction(QAbstractSlider::SliderPageStepSub); } setTextCursor(cursor); return true; } else if (KStandardShortcut::begin().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::Start); setTextCursor(cursor); return true; } else if (KStandardShortcut::end().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::End); setTextCursor(cursor); return true; } else if (KStandardShortcut::beginningOfLine().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::StartOfLine); setTextCursor(cursor); return true; } else if (KStandardShortcut::endOfLine().contains(key)) { QTextCursor cursor = textCursor(); cursor.movePosition(QTextCursor::EndOfLine); setTextCursor(cursor); return true; } else if (searchSupport() && KStandardShortcut::find().contains(key)) { Q_EMIT findText(); return true; } else if (searchSupport() && KStandardShortcut::replace().contains(key)) { if (!isReadOnly()) { Q_EMIT replaceText(); } return true; } else if (KStandardShortcut::pasteSelection().contains(key)) { QString text = QApplication::clipboard()->text(QClipboard::Selection); if (!text.isEmpty()) { insertPlainText(text); // TODO: check if this is html? (MiB) } return true; } else if (event == QKeySequence::DeleteEndOfLine) { QTextCursor cursor = textCursor(); QTextBlock block = cursor.block(); if (cursor.position() == block.position() + block.length() - 2) { cursor.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); } else { cursor.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); } cursor.removeSelectedText(); setTextCursor(cursor); return true; } return false; } bool RichTextEditor::overrideShortcut(QKeyEvent *event) { const int key = event->key() | event->modifiers(); if (KStandardShortcut::copy().contains(key)) { return true; } else if (KStandardShortcut::paste().contains(key)) { return true; } else if (KStandardShortcut::cut().contains(key)) { return true; } else if (KStandardShortcut::undo().contains(key)) { return true; } else if (KStandardShortcut::redo().contains(key)) { return true; } else if (KStandardShortcut::deleteWordBack().contains(key)) { return true; } else if (KStandardShortcut::deleteWordForward().contains(key)) { return true; } else if (KStandardShortcut::backwardWord().contains(key)) { return true; } else if (KStandardShortcut::forwardWord().contains(key)) { return true; } else if (KStandardShortcut::next().contains(key)) { return true; } else if (KStandardShortcut::prior().contains(key)) { return true; } else if (KStandardShortcut::begin().contains(key)) { return true; } else if (KStandardShortcut::end().contains(key)) { return true; } else if (KStandardShortcut::beginningOfLine().contains(key)) { return true; } else if (KStandardShortcut::endOfLine().contains(key)) { return true; } else if (KStandardShortcut::pasteSelection().contains(key)) { return true; } else if (searchSupport() && KStandardShortcut::find().contains(key)) { return true; } else if (searchSupport() && KStandardShortcut::findNext().contains(key)) { return true; } else if (searchSupport() && KStandardShortcut::replace().contains(key)) { return true; } else if (event->matches(QKeySequence::SelectAll)) { // currently missing in QTextEdit return true; } else if (event == QKeySequence::DeleteEndOfLine) { return true; } return false; } void RichTextEditor::keyPressEvent(QKeyEvent *event) { const bool isControlClicked = event->modifiers() & Qt::ControlModifier; const bool isShiftClicked = event->modifiers() & Qt::ShiftModifier; if (handleShortcut(event)) { event->accept(); } else if (event->key() == Qt::Key_Up && isControlClicked && isShiftClicked) { moveLineUpDown(true); event->accept(); } else if (event->key() == Qt::Key_Down && isControlClicked && isShiftClicked) { moveLineUpDown(false); event->accept(); } else { QTextEdit::keyPressEvent(event); } } int RichTextEditor::zoomFactor() const { int pourcentage = 100; QFont f = font(); if (d->mInitialFontSize != f.pointSize()) { pourcentage = (f.pointSize() * 100) / d->mInitialFontSize; } return pourcentage; } void RichTextEditor::slotZoomReset() { QFont f = font(); if (d->mInitialFontSize != f.pointSize()) { f.setPointSize(d->mInitialFontSize); setFont(f); } } void RichTextEditor::moveLineUpDown(bool moveUp) { QTextCursor cursor = textCursor(); QTextCursor move = cursor; move.beginEditBlock(); bool hasSelection = cursor.hasSelection(); if (hasSelection) { move.setPosition(cursor.selectionStart()); move.movePosition(QTextCursor::StartOfBlock); move.setPosition(cursor.selectionEnd(), QTextCursor::KeepAnchor); move.movePosition(move.atBlockStart() ? QTextCursor::Left : QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); } else { move.movePosition(QTextCursor::StartOfBlock); move.movePosition(QTextCursor::EndOfBlock, QTextCursor::KeepAnchor); } QString text = move.selectedText(); move.movePosition(QTextCursor::Right, QTextCursor::KeepAnchor); move.removeSelectedText(); if (moveUp) { move.movePosition(QTextCursor::PreviousBlock); move.insertBlock(); move.movePosition(QTextCursor::Left); } else { move.movePosition(QTextCursor::EndOfBlock); if (move.atBlockStart()) { // empty block move.movePosition(QTextCursor::NextBlock); move.insertBlock(); move.movePosition(QTextCursor::Left); } else { move.insertBlock(); } } int start = move.position(); move.clearSelection(); move.insertText(text); int end = move.position(); if (hasSelection) { move.setPosition(end); move.setPosition(start, QTextCursor::KeepAnchor); } else { move.setPosition(start); } move.endEditBlock(); setTextCursor(move); } diff --git a/src/texttospeech/autotests/texttospeechactionstest.cpp b/src/texttospeech/autotests/texttospeechactionstest.cpp index ca0ef8b..1b92d60 100644 --- a/src/texttospeech/autotests/texttospeechactionstest.cpp +++ b/src/texttospeech/autotests/texttospeechactionstest.cpp @@ -1,115 +1,115 @@ /* Copyright (C) 2014-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "texttospeechactionstest.h" #include "../texttospeechactions.h" #include #include #include Q_DECLARE_METATYPE(KPIMTextEdit::TextToSpeechWidget::State) TextToSpeechActionsTest::TextToSpeechActionsTest(QObject *parent) : QObject(parent) { qRegisterMetaType(); QIcon::setThemeName(QStringLiteral("breeze")); } TextToSpeechActionsTest::~TextToSpeechActionsTest() { } void TextToSpeechActionsTest::shouldHaveDefaultValue() { KPIMTextEdit::TextToSpeechActions act; QVERIFY(act.stopAction()); QVERIFY(act.playPauseAction()); QCOMPARE(act.state(), KPIMTextEdit::TextToSpeechWidget::Stop); QVERIFY(act.stopAction()->isEnabled()); QVERIFY(!act.stopAction()->icon().isNull()); QVERIFY(!act.playPauseAction()->isEnabled()); QVERIFY(!act.playPauseAction()->icon().isNull()); } void TextToSpeechActionsTest::shouldChangeButtonEnableStateWhenChangeState() { KPIMTextEdit::TextToSpeechActions act; act.setState(KPIMTextEdit::TextToSpeechWidget::Play); QVERIFY(act.stopAction()->isEnabled()); QVERIFY(act.playPauseAction()->isEnabled()); act.setState(KPIMTextEdit::TextToSpeechWidget::Pause); QVERIFY(act.stopAction()->isEnabled()); QVERIFY(act.playPauseAction()->isEnabled()); act.setState(KPIMTextEdit::TextToSpeechWidget::Stop); QVERIFY(act.stopAction()->isEnabled()); QVERIFY(!act.playPauseAction()->isEnabled()); } void TextToSpeechActionsTest::shouldChangeStateWhenClickOnPlayPause() { KPIMTextEdit::TextToSpeechActions act; act.setState(KPIMTextEdit::TextToSpeechWidget::Play); QCOMPARE(act.state(), KPIMTextEdit::TextToSpeechWidget::Play); act.playPauseAction()->trigger(); QCOMPARE(act.state(), KPIMTextEdit::TextToSpeechWidget::Pause); act.playPauseAction()->trigger(); QCOMPARE(act.state(), KPIMTextEdit::TextToSpeechWidget::Play); } void TextToSpeechActionsTest::shouldChangeStateWhenClickOnStop() { KPIMTextEdit::TextToSpeechActions act; act.setState(KPIMTextEdit::TextToSpeechWidget::Play); act.stopAction()->trigger(); QCOMPARE(act.state(), KPIMTextEdit::TextToSpeechWidget::Stop); } void TextToSpeechActionsTest::shouldEmitStateChanged() { KPIMTextEdit::TextToSpeechActions act; act.setState(KPIMTextEdit::TextToSpeechWidget::Play); - QSignalSpy spy(&act, SIGNAL(stateChanged(KPIMTextEdit::TextToSpeechWidget::State))); + QSignalSpy spy(&act, &KPIMTextEdit::TextToSpeechActions::stateChanged); act.setState(KPIMTextEdit::TextToSpeechWidget::Play); QCOMPARE(spy.count(), 0); act.playPauseAction()->trigger(); QCOMPARE(spy.count(), 1); QCOMPARE(spy.at(0).at(0).value(), KPIMTextEdit::TextToSpeechWidget::Pause); act.playPauseAction()->trigger(); QCOMPARE(spy.count(), 2); QCOMPARE(spy.at(1).at(0).value(), KPIMTextEdit::TextToSpeechWidget::Play); act.playPauseAction()->trigger(); QCOMPARE(spy.count(), 3); QCOMPARE(spy.at(2).at(0).value(), KPIMTextEdit::TextToSpeechWidget::Pause); act.stopAction()->trigger(); QCOMPARE(spy.count(), 4); QCOMPARE(spy.at(3).at(0).value(), KPIMTextEdit::TextToSpeechWidget::Stop); } QTEST_MAIN(TextToSpeechActionsTest) diff --git a/src/texttospeech/autotests/texttospeechconfigwidgettest.cpp b/src/texttospeech/autotests/texttospeechconfigwidgettest.cpp index a048ca7..7cc6b2c 100644 --- a/src/texttospeech/autotests/texttospeechconfigwidgettest.cpp +++ b/src/texttospeech/autotests/texttospeechconfigwidgettest.cpp @@ -1,99 +1,99 @@ /* Copyright (C) 2014-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "texttospeechconfigwidgettest.h" #include "texttospeech/texttospeechconfigwidget.h" #include "texttospeech/abstracttexttospeechconfiginterface.h" #include #include #include #include TextToSpeechConfigWidgetTest::TextToSpeechConfigWidgetTest(QObject *parent) : QObject(parent) { } TextToSpeechConfigWidgetTest::~TextToSpeechConfigWidgetTest() { } void TextToSpeechConfigWidgetTest::addInterface(KPIMTextEdit::TextToSpeechConfigWidget *widget) { KPIMTextEdit::AbstractTextToSpeechConfigInterface *interface = new KPIMTextEdit::AbstractTextToSpeechConfigInterface(this); widget->setTextToSpeechConfigInterface(interface); } void TextToSpeechConfigWidgetTest::shouldHaveDefaultValue() { KPIMTextEdit::TextToSpeechConfigWidget textToSpeechConfigWidget; addInterface(&textToSpeechConfigWidget); QSlider *volume = textToSpeechConfigWidget.findChild(QStringLiteral("volume")); QVERIFY(volume); QSlider *rate = textToSpeechConfigWidget.findChild(QStringLiteral("rate")); QVERIFY(rate); QSlider *pitch = textToSpeechConfigWidget.findChild(QStringLiteral("pitch")); QVERIFY(pitch); QComboBox *language = textToSpeechConfigWidget.findChild(QStringLiteral("language")); QVERIFY(language); //FIXME //QVERIFY(language->count()>0); QComboBox *availableEngine = textToSpeechConfigWidget.findChild(QStringLiteral("engine")); QVERIFY(availableEngine); QComboBox *voice = textToSpeechConfigWidget.findChild(QStringLiteral("voice")); QVERIFY(voice); } void TextToSpeechConfigWidgetTest::shouldEmitConfigChangedWhenChangeConfigValue() { KPIMTextEdit::TextToSpeechConfigWidget textToSpeechConfigWidget; addInterface(&textToSpeechConfigWidget); - QSignalSpy spy(&textToSpeechConfigWidget, SIGNAL(configChanged(bool))); + QSignalSpy spy(&textToSpeechConfigWidget, &KPIMTextEdit::TextToSpeechConfigWidget::configChanged); QSlider *volume = textToSpeechConfigWidget.findChild(QStringLiteral("volume")); volume->setValue(5); QCOMPARE(spy.count(), 1); QSlider *rate = textToSpeechConfigWidget.findChild(QStringLiteral("rate")); rate->setValue(5); QCOMPARE(spy.count(), 2); QSlider *pitch = textToSpeechConfigWidget.findChild(QStringLiteral("pitch")); pitch->setValue(5); QCOMPARE(spy.count(), 3); QComboBox *language = textToSpeechConfigWidget.findChild(QStringLiteral("language")); language->blockSignals(true); QStringList lst; lst << QStringLiteral("foo"); lst << QStringLiteral("foo"); lst << QStringLiteral("foo"); lst << QStringLiteral("foo"); language->addItems(lst); language->blockSignals(false); language->setCurrentIndex(3); QCOMPARE(spy.count(), 4); } QTEST_MAIN(TextToSpeechConfigWidgetTest) diff --git a/src/texttospeech/autotests/texttospeechwidgettest.cpp b/src/texttospeech/autotests/texttospeechwidgettest.cpp index 872d617..3797cfe 100644 --- a/src/texttospeech/autotests/texttospeechwidgettest.cpp +++ b/src/texttospeech/autotests/texttospeechwidgettest.cpp @@ -1,145 +1,145 @@ /* Copyright (C) 2014-2019 Laurent Montel This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "texttospeechwidgettest.h" #include "../abstracttexttospeechinterface.h" #include #include "texttospeech/texttospeechwidget.h" #include #include #include #include Q_DECLARE_METATYPE(KPIMTextEdit::TextToSpeechWidget::State) TextToSpeechWidgetTest::TextToSpeechWidgetTest(QObject *parent) : QObject(parent) { qRegisterMetaType(); QIcon::setThemeName(QStringLiteral("breeze")); } void TextToSpeechWidgetTest::addInterface(KPIMTextEdit::TextToSpeechWidget *widget) { KPIMTextEdit::AbstractTextToSpeechInterface *interface = new KPIMTextEdit::AbstractTextToSpeechInterface(this); widget->setTextToSpeechInterface(interface); } void TextToSpeechWidgetTest::shouldHaveDefaultValue() { KPIMTextEdit::TextToSpeechWidget textToSpeechWidget; addInterface(&textToSpeechWidget); QCOMPARE(textToSpeechWidget.state(), KPIMTextEdit::TextToSpeechWidget::Stop); QToolButton *closeButton = textToSpeechWidget.findChild(QStringLiteral("close-button")); QVERIFY(closeButton); QToolButton *stopButton = textToSpeechWidget.findChild(QStringLiteral("stopbutton")); QVERIFY(stopButton); QVERIFY(stopButton->isEnabled()); QVERIFY(!stopButton->icon().isNull()); QToolButton *playPauseButton = textToSpeechWidget.findChild(QStringLiteral("playpausebutton")); QVERIFY(playPauseButton); QVERIFY(!playPauseButton->isEnabled()); QVERIFY(!playPauseButton->icon().isNull()); QSlider *volume = textToSpeechWidget.findChild(QStringLiteral("volumeslider")); QVERIFY(volume); QToolButton *configureButton = textToSpeechWidget.findChild(QStringLiteral("configurebutton")); QVERIFY(configureButton); QVERIFY(!configureButton->icon().isNull()); } void TextToSpeechWidgetTest::shouldChangeButtonEnableStateWhenChangeState() { KPIMTextEdit::TextToSpeechWidget textToSpeechWidget; addInterface(&textToSpeechWidget); textToSpeechWidget.setState(KPIMTextEdit::TextToSpeechWidget::Play); QToolButton *stopButton = textToSpeechWidget.findChild(QStringLiteral("stopbutton")); QVERIFY(stopButton->isEnabled()); QToolButton *playPauseButton = textToSpeechWidget.findChild(QStringLiteral("playpausebutton")); QVERIFY(playPauseButton->isEnabled()); textToSpeechWidget.setState(KPIMTextEdit::TextToSpeechWidget::Pause); QVERIFY(stopButton->isEnabled()); QVERIFY(playPauseButton->isEnabled()); textToSpeechWidget.setState(KPIMTextEdit::TextToSpeechWidget::Stop); QVERIFY(stopButton->isEnabled()); QVERIFY(!playPauseButton->isEnabled()); } void TextToSpeechWidgetTest::shouldChangeStateWhenClickOnPlayPause() { KPIMTextEdit::TextToSpeechWidget textToSpeechWidget; addInterface(&textToSpeechWidget); textToSpeechWidget.setState(KPIMTextEdit::TextToSpeechWidget::Play); QToolButton *playPauseButton = textToSpeechWidget.findChild(QStringLiteral("playpausebutton")); QCOMPARE(textToSpeechWidget.state(), KPIMTextEdit::TextToSpeechWidget::Play); QTest::mouseClick(playPauseButton, Qt::LeftButton); QCOMPARE(textToSpeechWidget.state(), KPIMTextEdit::TextToSpeechWidget::Pause); QTest::mouseClick(playPauseButton, Qt::LeftButton); QCOMPARE(textToSpeechWidget.state(), KPIMTextEdit::TextToSpeechWidget::Play); } void TextToSpeechWidgetTest::shouldChangeStateWhenClickOnStop() { KPIMTextEdit::TextToSpeechWidget textToSpeechWidget; addInterface(&textToSpeechWidget); textToSpeechWidget.setState(KPIMTextEdit::TextToSpeechWidget::Play); QToolButton *stopButton = textToSpeechWidget.findChild(QStringLiteral("stopbutton")); QTest::mouseClick(stopButton, Qt::LeftButton); QCOMPARE(textToSpeechWidget.state(), KPIMTextEdit::TextToSpeechWidget::Stop); } void TextToSpeechWidgetTest::shouldEmitStateChanged() { KPIMTextEdit::TextToSpeechWidget textToSpeechWidget; addInterface(&textToSpeechWidget); - QSignalSpy spy(&textToSpeechWidget, SIGNAL(stateChanged(KPIMTextEdit::TextToSpeechWidget::State))); + QSignalSpy spy(&textToSpeechWidget, &KPIMTextEdit::TextToSpeechWidget::stateChanged); textToSpeechWidget.setState(KPIMTextEdit::TextToSpeechWidget::Play); QCOMPARE(spy.count(), 0); QToolButton *stopButton = textToSpeechWidget.findChild(QStringLiteral("stopbutton")); QToolButton *playPauseButton = textToSpeechWidget.findChild(QStringLiteral("playpausebutton")); QTest::mouseClick(playPauseButton, Qt::LeftButton); QCOMPARE(spy.count(), 1); QCOMPARE(spy.at(0).at(0).value(), KPIMTextEdit::TextToSpeechWidget::Pause); QTest::mouseClick(playPauseButton, Qt::LeftButton); QCOMPARE(spy.count(), 2); QCOMPARE(spy.at(1).at(0).value(), KPIMTextEdit::TextToSpeechWidget::Play); QTest::mouseClick(playPauseButton, Qt::LeftButton); QCOMPARE(spy.count(), 3); QCOMPARE(spy.at(2).at(0).value(), KPIMTextEdit::TextToSpeechWidget::Pause); QTest::mouseClick(stopButton, Qt::LeftButton); QCOMPARE(spy.count(), 4); QCOMPARE(spy.at(3).at(0).value(), KPIMTextEdit::TextToSpeechWidget::Stop); } QTEST_MAIN(TextToSpeechWidgetTest)