diff --git a/autotests/krichtextedittest.cpp b/autotests/krichtextedittest.cpp index 80da3c4..e497a04 100644 --- a/autotests/krichtextedittest.cpp +++ b/autotests/krichtextedittest.cpp @@ -1,366 +1,472 @@ /* This file is part of the KDE libraries Copyright (c) 2009 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), 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 Lesser 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 "krichtextedittest.h" #include #include #include #include #include #include #include #include QTEST_MAIN(KRichTextEditTest) void KRichTextEditTest::testLinebreaks() { KRichTextEdit edit; edit.enableRichTextMode(); // Enter the text with keypresses, for some strange reason a normal setText() or // setPlainText() call doesn't do the trick QTest::keyClicks(&edit, QStringLiteral("a\r\r")); edit.setTextUnderline(true); QTest::keyClicks(&edit, QStringLiteral("b\r\r\rc")); QCOMPARE(edit.toPlainText(), QStringLiteral("a\n\nb\n\n\nc")); QString html = edit.toCleanHtml(); edit.clear(); edit.setHtml(html); QCOMPARE(edit.toPlainText(), QStringLiteral("a\n\nb\n\n\nc")); } void KRichTextEditTest::testUpdateLinkAdd() { KRichTextEdit edit; edit.enableRichTextMode(); // Add text, apply initial formatting, and add a link QTextCursor cursor = edit.textCursor(); cursor.insertText(QStringLiteral("Test")); QTextCharFormat charFormat = cursor.charFormat(); // Note that QTextEdit doesn't use the palette. Black is black. QCOMPARE(charFormat.foreground().color().name(), QColor(Qt::black).name()); cursor.select(QTextCursor::BlockUnderCursor); edit.setTextCursor(cursor); edit.setTextBold(true); edit.setTextItalic(true); edit.updateLink(QStringLiteral("http://www.kde.org"), QStringLiteral("KDE")); // Validate text and formatting cursor.movePosition(QTextCursor::Start); cursor.select(QTextCursor::WordUnderCursor); edit.setTextCursor(cursor); QCOMPARE(edit.toPlainText(), QStringLiteral("KDE ")); QCOMPARE(edit.fontItalic(), true); QCOMPARE(edit.fontWeight(), static_cast(QFont::Bold)); QCOMPARE(edit.fontUnderline(), true); charFormat = cursor.charFormat(); QCOMPARE(charFormat.foreground(), QBrush(KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color())); QCOMPARE(charFormat.underlineColor(), KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color()); QCOMPARE(charFormat.underlineStyle(), QTextCharFormat::SingleUnderline); } void KRichTextEditTest::testUpdateLinkRemove() { KRichTextEdit edit; edit.enableRichTextMode(); // Add text, apply initial formatting, and add a link QTextCursor cursor = edit.textCursor(); cursor.insertText(QStringLiteral("Test")); cursor.select(QTextCursor::BlockUnderCursor); edit.setTextCursor(cursor); edit.setTextBold(true); edit.setTextItalic(true); edit.updateLink(QStringLiteral("http://www.kde.org"), QStringLiteral("KDE")); // Remove link and validate formatting cursor.movePosition(QTextCursor::Start); cursor.select(QTextCursor::WordUnderCursor); edit.setTextCursor(cursor); edit.updateLink(QString(), QStringLiteral("KDE")); cursor.movePosition(QTextCursor::Start); cursor.select(QTextCursor::WordUnderCursor); edit.setTextCursor(cursor); QCOMPARE(edit.toPlainText(), QStringLiteral("KDE ")); QCOMPARE(edit.fontItalic(), true); QCOMPARE(edit.fontWeight(), static_cast(QFont::Bold)); QCOMPARE(edit.fontUnderline(), false); QTextCharFormat charFormat = cursor.charFormat(); QCOMPARE(charFormat.foreground().color().name(), QColor(Qt::black).name()); QCOMPARE(charFormat.underlineColor().name(), QColor(Qt::black).name()); QCOMPARE(charFormat.underlineStyle(), QTextCharFormat::NoUnderline); } void KRichTextEditTest::testHTMLLineBreaks() { KRichTextEdit edit; edit.enableRichTextMode(); // Create the following text: //A // //B QTest::keyClicks(&edit, QStringLiteral("a\r")); edit.setTextUnderline(true); QTest::keyClicks(&edit, QStringLiteral("\rb")); QString html = edit.toCleanHtml(); // The problem we have is that we need to "fake" being a viewer such // as Thunderbird or MS-Outlook to unit test our html line breaks. // For now, we'll parse the 6th line (the empty one) and make sure it has the proper format // The first four (4) HTML code lines are DOCTYPE through declaration const QStringList lines = html.split(QLatin1Char('\n')); // for (int idx=0; idx line QVERIFY(line6.startsWith(QStringLiteral("

 

")), "Empty lines must have   or otherwise 3rd party " "viewers render those as non-existing lines"); } void KRichTextEditTest::testHTMLOrderedLists() { // The problem we have is that we need to "fake" being a viewer such // as Thunderbird or MS-Outlook to unit test our html lists. // For now, we'll parse the 6th line (the
    element) and make sure it has the proper format KRichTextEdit edit; edit.enableRichTextMode(); edit.setTextUnderline(true); // create a numbered (ordered) list QTextCursor cursor = edit.textCursor(); cursor.insertList(QTextListFormat::ListDecimal); QTest::keyClicks(&edit, QStringLiteral("a\rb\rc\r")); QString html = edit.toCleanHtml(); const QStringList lines = html.split(QLatin1Char('\n')); // Uncomment this section in case the first test fails to see if the HTML // rendering has actually introduced a bug, or merely a problem with the unit test itself // // for (int idx=0; idx declaration line const QString &line6 = lines.at(5); // qDebug() << line6; // there should not be a margin-left: 0 defined for the
      element QRegExp regex(QStringLiteral(" element) and make sure it has the proper format // The first four (4) HTML code lines are DOCTYPE through declaration KRichTextEdit edit; edit.enableRichTextMode(); edit.setTextUnderline(true); // create a numbered (ordered) list QTextCursor cursor = edit.textCursor(); cursor.insertList(QTextListFormat::ListDisc); QTest::keyClicks(&edit, QStringLiteral("a\rb\rc\r")); QString html = edit.toCleanHtml(); const QStringList lines = html.split(QLatin1Char('\n')); // Uncomment this section in case the first test fails to see if the HTML // rendering has actually introduced a bug, or merely a problem with the unit test itself // // for (int idx=0; idx declaration line const QString &line6 = lines.at(5); // qDebug() << line6; // there should not be a margin-left: 0 defined for the
        element QRegExp regex(QStringLiteral("(QFont::Bold)); // Make sure it doesn't clutter undo stack (a single undo is sufficient) edit.undo(); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0); QCOMPARE(edit.fontWeight(), static_cast(QFont::Normal)); // Set heading & keep writing, the text remains a heading edit.setHeadingLevel(2); QTest::keyClicks(&edit, QStringLiteral("cd")); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 2); QCOMPARE(edit.fontWeight(), static_cast(QFont::Bold)); // Now add a new line, make sure it's no longer a heading QTest::keyClick(&edit, '\r'); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0); QCOMPARE(edit.fontWeight(), static_cast(QFont::Normal)); // Make sure creating new line is also undoable edit.undo(); QCOMPARE(edit.textCursor().position(), 5); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 2); QCOMPARE(edit.fontWeight(), static_cast(QFont::Bold)); // Add a new line and some more text, make sure it's still normal QTest::keyClicks(&edit, QStringLiteral("\ref")); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0); QCOMPARE(edit.fontWeight(), static_cast(QFont::Normal)); // Go to beginning of this line, press Backspace -> lines should be merged, // current line should become a heading QTest::keyClick(&edit, Qt::Key_Home); QTest::keyClick(&edit, Qt::Key_Backspace); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 2); QCOMPARE(edit.fontWeight(), static_cast(QFont::Bold)); // Make sure this is also undoable edit.undo(); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0); QCOMPARE(edit.fontWeight(), static_cast(QFont::Normal)); // Return it back QTest::keyClick(&edit, Qt::Key_Backspace); // The line is now "bcd|ef", "|" is cursor. Press Enter, the second line should remain a heading QTest::keyClick(&edit, Qt::Key_Return); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 2); QCOMPARE(edit.fontWeight(), static_cast(QFont::Bold)); // Change the heading level back to normal edit.setHeadingLevel(0); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0); QCOMPARE(edit.fontWeight(), static_cast(QFont::Normal)); // Go to end of previous line, press Delete -> lines should be merged again QTextCursor cursor = edit.textCursor(); cursor.movePosition(QTextCursor::PreviousBlock); cursor.movePosition(QTextCursor::EndOfBlock); edit.setTextCursor(cursor); QTest::keyClick(&edit, Qt::Key_Delete); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 2); QCOMPARE(edit.fontWeight(), static_cast(QFont::Bold)); // Make sure this is also undoable edit.undo(); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 0); QCOMPARE(edit.fontWeight(), static_cast(QFont::Normal)); // Now playing with selection. The contents are currently: // --- // a // bcd // ef // gh // --- // Let's add a new line 'gh', select everything between "c" and "g" // and change heading. It should apply to both lines QTest::keyClicks(&edit, QStringLiteral("\rgh")); cursor.setPosition(4, QTextCursor::MoveAnchor); cursor.setPosition(10, QTextCursor::KeepAnchor); edit.setTextCursor(cursor); edit.setHeadingLevel(5); // In the end, both lines should change the heading (even before the selection, i.e. 'b'!) cursor.setPosition(3); edit.setTextCursor(cursor); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 5); QCOMPARE(edit.fontWeight(), static_cast(QFont::Bold)); // (and after the selection, i.e. 'f'!) cursor.setPosition(11); edit.setTextCursor(cursor); QCOMPARE(edit.textCursor().blockFormat().headingLevel(), 5); QCOMPARE(edit.fontWeight(), static_cast(QFont::Bold)); } void KRichTextEditTest::testRulerScroll() { // This is a test for bug 195828 KRichTextEdit edit; // Add some lines, so that scroll definitely appears for (int i = 0; i < 100; i++) { QTest::keyClicks(&edit, QStringLiteral("New line\r")); } // Widget has to be shown for the scrollbar to be adjusted edit.show(); // Ensure the scrollbar actually appears QVERIFY(edit.verticalScrollBar()->value() > 0); edit.insertHorizontalRule(); // Make sure scrollbar didn't jump to the top QVERIFY(edit.verticalScrollBar()->value() > 0); } + +void KRichTextEditTest::testNestedLists() +{ + KRichTextEdit edit; + // Simplest test: create a list with a single element + QTest::keyClicks(&edit, QStringLiteral("el1")); + edit.setListStyle(-static_cast(QTextListFormat::ListSquare)); + QVERIFY(edit.textCursor().currentList()); + QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListSquare); + // It should not be indentable, as there is nothing above + QVERIFY(!edit.canIndentList()); + // But it should be dedentable + QVERIFY(edit.canDedentList()); + // Press enter, a new element should be added + QTest::keyClicks(&edit, QStringLiteral("\rel2")); + QVERIFY(edit.textCursor().currentList()); + QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListSquare); + // Change indentation + edit.indentListMore(); + edit.setListStyle(-static_cast(QTextListFormat::ListCircle)); + QCOMPARE(edit.textCursor().currentList()->format().indent(), 2); + QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListCircle); + // And another one; let's then change the style of "3" and see if "2" have also changed style + QTest::keyClicks(&edit, QStringLiteral("\rel3")); + edit.setListStyle(-static_cast(QTextListFormat::ListDecimal)); + edit.moveCursor(QTextCursor::PreviousBlock); + QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListDecimal); + // Now add another element, and dedent it, so the list should look like following: + // [] el1 + // 1. el2 + // 2. el3 + // [] el4 + edit.moveCursor(QTextCursor::End); + QTest::keyClicks(&edit, QStringLiteral("\rel4")); + edit.indentListLess(); + QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListSquare); + // Let's change the style to disc and see if first element have also changed the style + edit.setListStyle(-static_cast(QTextListFormat::ListDisc)); + edit.moveCursor(QTextCursor::Start); + QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListDisc); + // Now let's play with selection. First we add couple subelements below, so the list is: + // * el1 + // 1. el2 + // 2. el3 + // * el4 + // o el5 + // o el6 + edit.moveCursor(QTextCursor::End); + QTest::keyClicks(&edit, QStringLiteral("\rel5")); + edit.indentListMore(); + edit.setListStyle(-static_cast(QTextListFormat::ListCircle)); + QTest::keyClicks(&edit, QStringLiteral("\rel6")); + + // Let's select (el3-el5) and indent them. It should become: + // * el1 + // 1. el2 + // 1. el3 + // 2. el4 + // o el5 + // 3. el6 + QTextCursor cursor(edit.document()); + cursor.setPosition(9); + cursor.setPosition(17, QTextCursor::KeepAnchor); + edit.setTextCursor(cursor); + edit.indentListMore(); + edit.moveCursor(QTextCursor::End); + QCOMPARE(edit.textCursor().currentList()->count(), 3); + QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListDecimal); + // Now select el2-el5 and dedent them. It should become: + // * el1 + // * el2 + // 1. el3 + // * el4 + // o el5 + // o el6 + cursor.setPosition(6); + cursor.setPosition(18, QTextCursor::KeepAnchor); + edit.setTextCursor(cursor); + edit.indentListLess(); + edit.moveCursor(QTextCursor::End); + QCOMPARE(edit.textCursor().currentList()->count(), 2); + QCOMPARE(edit.textCursor().currentList()->format().style(), QTextListFormat::ListCircle); + // point at "el4" + cursor.setPosition(13); + QCOMPARE(cursor.currentList()->count(), 3); + QCOMPARE(cursor.currentList()->format().style(), QTextListFormat::ListDisc); + // Select el4 && el5, dedent it, so el4 becomes a simple text: + // * el1 + // * el2 + // 1. el3 + // el4 + // o el5 + // o el6 + cursor.setPosition(17, QTextCursor::KeepAnchor); + edit.setTextCursor(cursor); + edit.indentListLess(); + // point cursor at "el4" + cursor.setPosition(13); + QVERIFY(!cursor.currentList()); + // point at "el5", make sure it's a separate list now + cursor.setPosition(16); + QCOMPARE(cursor.currentList()->count(), 1); + QCOMPARE(cursor.currentList()->format().style(), QTextListFormat::ListCircle); + // Make sure the selection is not dedentable anymore + QVERIFY(!edit.canDedentList()); +} diff --git a/autotests/krichtextedittest.h b/autotests/krichtextedittest.h index 15d7df1..8fc998c 100644 --- a/autotests/krichtextedittest.h +++ b/autotests/krichtextedittest.h @@ -1,40 +1,41 @@ /* This file is part of the KDE libraries Copyright (c) 2009 Thomas McGuire This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), 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 Lesser 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. */ #ifndef KRICHTEXTEDITTEST_H #define KRICHTEXTEDITTEST_H #include class KRichTextEditTest : public QObject { Q_OBJECT private Q_SLOTS: void testLinebreaks(); void testUpdateLinkAdd(); void testUpdateLinkRemove(); void testHTMLLineBreaks(); void testHTMLOrderedLists(); void testHTMLUnorderedLists(); void testHeading(); void testRulerScroll(); + void testNestedLists(); }; #endif diff --git a/src/widgets/krichtextedit.cpp b/src/widgets/krichtextedit.cpp index 3d84935..2171b8f 100644 --- a/src/widgets/krichtextedit.cpp +++ b/src/widgets/krichtextedit.cpp @@ -1,641 +1,641 @@ /* * krichtextedit * * Copyright 2007 Laurent Montel * Copyright 2008 Thomas McGuire * Copyright 2008 Stephen Kelly * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "krichtextedit.h" // Own includes #include "nestedlisthelper_p.h" #include "klinkdialog_p.h" // kdelibs includes #include #include // Qt includes #include /** Private class that helps to provide binary compatibility between releases. @internal */ //@cond PRIVATE class KRichTextEditPrivate : public QObject { public: explicit KRichTextEditPrivate(KRichTextEdit *parent) : q(parent), mMode(KRichTextEdit::Plain) { nestedListHelper = new NestedListHelper(q); } ~KRichTextEditPrivate() { delete nestedListHelper; } // // Normal functions // // If the text under the cursor is a link, the cursor's selection is set to // the complete link text. Otherwise selects the current word if there is no // selection. void selectLinkText() const; void init(); // Switches to rich text mode and emits the mode changed signal if the // mode really changed. void activateRichText(); // Applies formatting to the current word if there is no selection. void mergeFormatOnWordOrSelection(const QTextCharFormat &format); void setTextCursor(QTextCursor &cursor); // Data members KRichTextEdit *q; KRichTextEdit::Mode mMode; NestedListHelper *nestedListHelper; }; void KRichTextEditPrivate::activateRichText() { if (mMode == KRichTextEdit::Plain) { q->setAcceptRichText(true); mMode = KRichTextEdit::Rich; emit q->textModeChanged(mMode); } } void KRichTextEditPrivate::setTextCursor(QTextCursor &cursor) { q->setTextCursor(cursor); } void KRichTextEditPrivate::mergeFormatOnWordOrSelection(const QTextCharFormat &format) { QTextCursor cursor = q->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); q->mergeCurrentCharFormat(format); cursor.endEditBlock(); } //@endcond KRichTextEdit::KRichTextEdit(const QString &text, QWidget *parent) : KTextEdit(text, parent), d(new KRichTextEditPrivate(this)) { d->init(); } KRichTextEdit::KRichTextEdit(QWidget *parent) : KTextEdit(parent), d(new KRichTextEditPrivate(this)) { d->init(); } KRichTextEdit::~KRichTextEdit() { delete d; } //@cond PRIVATE void KRichTextEditPrivate::init() { q->setAcceptRichText(false); KCursor::setAutoHideCursor(q, true, true); } //@endcond void KRichTextEdit::setListStyle(int _styleIndex) { d->nestedListHelper->handleOnBulletType(-_styleIndex); setFocus(); d->activateRichText(); } void KRichTextEdit::indentListMore() { - d->nestedListHelper->handleOnIndentMore(); + d->nestedListHelper->changeIndent(+1); d->activateRichText(); } void KRichTextEdit::indentListLess() { - d->nestedListHelper->handleOnIndentLess(); + d->nestedListHelper->changeIndent(-1); } void KRichTextEdit::insertHorizontalRule() { QTextCursor cursor = textCursor(); QTextBlockFormat bf = cursor.blockFormat(); QTextCharFormat cf = cursor.charFormat(); cursor.beginEditBlock(); cursor.insertHtml(QStringLiteral("
        ")); cursor.insertBlock(bf, cf); cursor.endEditBlock(); setTextCursor(cursor); d->activateRichText(); } void KRichTextEdit::alignLeft() { setAlignment(Qt::AlignLeft); setFocus(); d->activateRichText(); } void KRichTextEdit::alignCenter() { setAlignment(Qt::AlignHCenter); setFocus(); d->activateRichText(); } void KRichTextEdit::alignRight() { setAlignment(Qt::AlignRight); setFocus(); d->activateRichText(); } void KRichTextEdit::alignJustify() { setAlignment(Qt::AlignJustify); setFocus(); d->activateRichText(); } void KRichTextEdit::makeRightToLeft() { QTextBlockFormat format; format.setLayoutDirection(Qt::RightToLeft); QTextCursor cursor = textCursor(); cursor.mergeBlockFormat(format); setTextCursor(cursor); setFocus(); d->activateRichText(); } void KRichTextEdit::makeLeftToRight() { QTextBlockFormat format; format.setLayoutDirection(Qt::LeftToRight); QTextCursor cursor = textCursor(); cursor.mergeBlockFormat(format); setTextCursor(cursor); setFocus(); d->activateRichText(); } void KRichTextEdit::setTextBold(bool bold) { QTextCharFormat fmt; fmt.setFontWeight(bold ? QFont::Bold : QFont::Normal); d->mergeFormatOnWordOrSelection(fmt); setFocus(); d->activateRichText(); } void KRichTextEdit::setTextItalic(bool italic) { QTextCharFormat fmt; fmt.setFontItalic(italic); d->mergeFormatOnWordOrSelection(fmt); setFocus(); d->activateRichText(); } void KRichTextEdit::setTextUnderline(bool underline) { QTextCharFormat fmt; fmt.setFontUnderline(underline); d->mergeFormatOnWordOrSelection(fmt); setFocus(); d->activateRichText(); } void KRichTextEdit::setTextStrikeOut(bool strikeOut) { QTextCharFormat fmt; fmt.setFontStrikeOut(strikeOut); d->mergeFormatOnWordOrSelection(fmt); setFocus(); d->activateRichText(); } void KRichTextEdit::setTextForegroundColor(const QColor &color) { QTextCharFormat fmt; fmt.setForeground(color); d->mergeFormatOnWordOrSelection(fmt); setFocus(); d->activateRichText(); } void KRichTextEdit::setTextBackgroundColor(const QColor &color) { QTextCharFormat fmt; fmt.setBackground(color); d->mergeFormatOnWordOrSelection(fmt); setFocus(); d->activateRichText(); } void KRichTextEdit::setFontFamily(const QString &fontFamily) { QTextCharFormat fmt; fmt.setFontFamily(fontFamily); d->mergeFormatOnWordOrSelection(fmt); setFocus(); d->activateRichText(); } void KRichTextEdit::setFontSize(int size) { QTextCharFormat fmt; fmt.setFontPointSize(size); d->mergeFormatOnWordOrSelection(fmt); setFocus(); d->activateRichText(); } void KRichTextEdit::setFont(const QFont &font) { QTextCharFormat fmt; fmt.setFont(font); d->mergeFormatOnWordOrSelection(fmt); setFocus(); d->activateRichText(); } void KRichTextEdit::switchToPlainText() { if (d->mMode == Rich) { d->mMode = Plain; // TODO: Warn the user about this? QMetaObject::invokeMethod(this, "insertPlainTextImplementation"); setAcceptRichText(false); emit textModeChanged(d->mMode); } } void KRichTextEdit::insertPlainTextImplementation() { document()->setPlainText(document()->toPlainText()); } void KRichTextEdit::setTextSuperScript(bool superscript) { QTextCharFormat fmt; fmt.setVerticalAlignment(superscript ? QTextCharFormat::AlignSuperScript : QTextCharFormat::AlignNormal); d->mergeFormatOnWordOrSelection(fmt); setFocus(); d->activateRichText(); } void KRichTextEdit::setTextSubScript(bool subscript) { QTextCharFormat fmt; fmt.setVerticalAlignment(subscript ? QTextCharFormat::AlignSubScript : QTextCharFormat::AlignNormal); d->mergeFormatOnWordOrSelection(fmt); setFocus(); d->activateRichText(); } void KRichTextEdit::setHeadingLevel(int level) { const int boundedLevel = qBound(0, 6, level); // Apparently, 5 is maximum for FontSizeAdjustment; otherwise level=1 and // level=2 look the same const int sizeAdjustment = boundedLevel > 0 ? 5 - boundedLevel: 0; QTextCursor cursor = textCursor(); cursor.beginEditBlock(); QTextBlockFormat blkfmt; blkfmt.setHeadingLevel(boundedLevel); cursor.mergeBlockFormat(blkfmt); QTextCharFormat chrfmt; chrfmt.setFontWeight(boundedLevel > 0 ? QFont::Bold : QFont::Normal); chrfmt.setProperty(QTextFormat::FontSizeAdjustment, sizeAdjustment); // Applying style to the current line or selection QTextCursor selectCursor = cursor; if (selectCursor.hasSelection()) { QTextCursor top = selectCursor; top.setPosition(qMin(top.anchor(), top.position())); top.movePosition(QTextCursor::StartOfBlock); QTextCursor bottom = selectCursor; bottom.setPosition(qMax(bottom.anchor(), bottom.position())); bottom.movePosition(QTextCursor::EndOfBlock); selectCursor.setPosition(top.position(), QTextCursor::MoveAnchor); selectCursor.setPosition(bottom.position(), QTextCursor::KeepAnchor); } else { selectCursor.select(QTextCursor::BlockUnderCursor); } selectCursor.mergeCharFormat(chrfmt); cursor.mergeBlockCharFormat(chrfmt); cursor.endEditBlock(); setTextCursor(cursor); setFocus(); d->activateRichText(); } void KRichTextEdit::enableRichTextMode() { d->activateRichText(); } KRichTextEdit::Mode KRichTextEdit::textMode() const { return d->mMode; } QString KRichTextEdit::textOrHtml() const { if (textMode() == Rich) { return toCleanHtml(); } else { return toPlainText(); } } void KRichTextEdit::setTextOrHtml(const QString &text) { // might be rich text if (Qt::mightBeRichText(text)) { if (d->mMode == KRichTextEdit::Plain) { d->activateRichText(); } setHtml(text); } else { setPlainText(text); } } QString KRichTextEdit::currentLinkText() const { QTextCursor cursor = textCursor(); selectLinkText(&cursor); return cursor.selectedText(); } void KRichTextEdit::selectLinkText() const { QTextCursor cursor = textCursor(); selectLinkText(&cursor); d->setTextCursor(cursor); } void KRichTextEdit::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; } cursor->setPosition(cursor->position() + 1, QTextCursor::KeepAnchor); } if (cursor->charFormat().anchorHref() != aHref) { cursor->setPosition(cursor->position() - 1, QTextCursor::KeepAnchor); } } else if (cursor->hasSelection()) { // Nothing to to. Using the currently selected text as the link text. } else { // Select current word cursor->movePosition(QTextCursor::StartOfWord); cursor->movePosition(QTextCursor::EndOfWord, QTextCursor::KeepAnchor); } } QString KRichTextEdit::currentLinkUrl() const { return textCursor().charFormat().anchorHref(); } void KRichTextEdit::updateLink(const QString &linkUrl, const QString &linkText) { selectLinkText(); QTextCursor cursor = 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(KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color()); format.setForeground(KColorScheme(QPalette::Active, KColorScheme::View).foreground(KColorScheme::LinkText).color()); d->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(); } void KRichTextEdit::keyPressEvent(QKeyEvent *event) { bool handled = false; if (textCursor().currentList()) { handled = d->nestedListHelper->handleKeyPressEvent(event); } // If a line was merged with previous (next) one, with different heading level, // the style should also be adjusted accordingly (i.e. merged) if ((event->key() == Qt::Key_Backspace && textCursor().atBlockStart() && (textCursor().blockFormat().headingLevel() != textCursor().block().previous().blockFormat().headingLevel())) || (event->key() == Qt::Key_Delete && textCursor().atBlockEnd() && (textCursor().blockFormat().headingLevel() != textCursor().block().next().blockFormat().headingLevel()))) { QTextCursor cursor = textCursor(); cursor.beginEditBlock(); if (event->key() == Qt::Key_Delete) { cursor.deleteChar(); } else { cursor.deletePreviousChar(); } setHeadingLevel(cursor.blockFormat().headingLevel()); cursor.endEditBlock(); handled = true; } if (!handled) { KTextEdit::keyPressEvent(event); } // Match the behavior of office suites: newline after header switches to normal text if ((event->key() == Qt::Key_Return) && (textCursor().blockFormat().headingLevel() > 0) && (textCursor().atBlockEnd())) { // it should be undoable together with actual "return" keypress textCursor().joinPreviousEditBlock(); setHeadingLevel(0); textCursor().endEditBlock(); } emit cursorPositionChanged(); } // void KRichTextEdit::dropEvent(QDropEvent *event) // { // int dropSize = event->mimeData()->text().size(); // // dropEvent( event ); // QTextCursor cursor = textCursor(); // int cursorPosition = cursor.position(); // cursor.setPosition( cursorPosition - dropSize ); // cursor.setPosition( cursorPosition, QTextCursor::KeepAnchor ); // setTextCursor( cursor ); // d->nestedListHelper->handleAfterDropEvent( event ); // } bool KRichTextEdit::canIndentList() const { return d->nestedListHelper->canIndent(); } bool KRichTextEdit::canDedentList() const { return d->nestedListHelper->canDedent(); } QString KRichTextEdit::toCleanHtml() const { QString result = toHtml(); static const QString EMPTYLINEHTML = QLatin1String( "

         

        "); // 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

           

          . // replace all occurrences with the new line text result.replace(QRegularExpression(EMPTYLINEREGEX), EMPTYLINEHTML); // 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; } diff --git a/src/widgets/nestedlisthelper.cpp b/src/widgets/nestedlisthelper.cpp index 1104499..c1134c5 100644 --- a/src/widgets/nestedlisthelper.cpp +++ b/src/widgets/nestedlisthelper.cpp @@ -1,289 +1,300 @@ /** * Nested list helper * * Copyright 2008 Stephen Kelly * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ #include "nestedlisthelper_p.h" #include #include #include #include #include "ktextedit.h" NestedListHelper::NestedListHelper(QTextEdit *te) { textEdit = te; } NestedListHelper::~NestedListHelper() { } bool NestedListHelper::handleKeyPressEvent(QKeyEvent *event) { QTextCursor cursor = textEdit->textCursor(); if (!cursor.currentList()) { return false; } if (event->key() == Qt::Key_Backspace && !cursor.hasSelection() && cursor.atBlockStart() && canDedent()) { - handleOnIndentLess(); + changeIndent(-1); return true; } if (event->key() == Qt::Key_Return && !cursor.hasSelection() && cursor.block().text().isEmpty() && canDedent()) { - handleOnIndentLess(); + changeIndent(-1); return true; } if (event->key() == Qt::Key_Tab && (cursor.atBlockStart() || cursor.hasSelection()) && canIndent()) { - handleOnIndentMore(); + changeIndent(+1); return true; } return false; } bool NestedListHelper::canIndent() const { const QTextCursor cursor = topOfSelection(); const QTextBlock block = cursor.block(); if (!block.isValid()) { return false; } if (!block.textList()) { return true; } const QTextBlock prevBlock = block.previous(); if (!prevBlock.textList()) { return false; } return block.textList()->format().indent() <= prevBlock.textList()->format().indent(); } bool NestedListHelper::canDedent() const { const QTextCursor cursor = bottomOfSelection(); const QTextBlock block = cursor.block(); if (!block.isValid()) { return false; } if (!block.textList() || block.textList()->format().indent() <= 0) { return false; } const QTextBlock nextBlock = block.next(); if (!nextBlock.textList()) { return true; } return block.textList()->format().indent() >= nextBlock.textList()->format().indent(); } bool NestedListHelper::handleAfterDropEvent(QDropEvent *dropEvent) { Q_UNUSED(dropEvent); QTextCursor cursor = topOfSelection(); QTextBlock droppedBlock = cursor.block(); int firstDroppedItemIndent = droppedBlock.textList()->format().indent(); int minimumIndent = droppedBlock.previous().textList()->format().indent(); if (firstDroppedItemIndent < minimumIndent) { cursor = QTextCursor(droppedBlock); QTextListFormat fmt = droppedBlock.textList()->format(); fmt.setIndent(minimumIndent); QTextList *list = cursor.createList(fmt); int endOfDrop = bottomOfSelection().position(); while (droppedBlock.next().position() < endOfDrop) { droppedBlock = droppedBlock.next(); if (droppedBlock.textList()->format().indent() != firstDroppedItemIndent) { // new list? } list->add(droppedBlock); } // list.add( droppedBlock ); } return true; } void NestedListHelper::processList(QTextList *list) { QTextBlock block = list->item(0); int thisListIndent = list->format().indent(); QTextCursor cursor = QTextCursor(block); list = cursor.createList(list->format()); bool processingSubList = false; while (block.next().textList() != nullptr) { block = block.next(); QTextList *nextList = block.textList(); int nextItemIndent = nextList->format().indent(); if (nextItemIndent < thisListIndent) { return; } else if (nextItemIndent > thisListIndent) { if (processingSubList) { continue; } processingSubList = true; processList(nextList); } else { processingSubList = false; list->add(block); } } // delete nextList; // nextList = 0; } void NestedListHelper::reformatList(QTextBlock block) { if (block.textList()) { int minimumIndent = block.textList()->format().indent(); // Start at the top of the list while (block.previous().textList() != nullptr) { if (block.previous().textList()->format().indent() < minimumIndent) { break; } block = block.previous(); } processList(block.textList()); } } void NestedListHelper::reformatList() { QTextCursor cursor = textEdit->textCursor(); reformatList(cursor.block()); } QTextCursor NestedListHelper::topOfSelection() const { QTextCursor cursor = textEdit->textCursor(); if (cursor.hasSelection()) { cursor.setPosition(qMin(cursor.position(), cursor.anchor())); } return cursor; } QTextCursor NestedListHelper::bottomOfSelection() const { QTextCursor cursor = textEdit->textCursor(); if (cursor.hasSelection()) { cursor.setPosition(qMax(cursor.position(), cursor.anchor())); } return cursor; } -void NestedListHelper::handleOnIndentMore() +void NestedListHelper::changeIndent(int delta) { QTextCursor cursor = textEdit->textCursor(); cursor.beginEditBlock(); - QTextListFormat listFmt; - if (!cursor.currentList()) { - - QTextListFormat::Style style; - cursor = topOfSelection(); - cursor.movePosition(QTextCursor::PreviousBlock); - if (cursor.currentList()) { - style = cursor.currentList()->format().style(); - } else { + const int top = qMin(cursor.position(), cursor.anchor()); + const int bottom = qMax(cursor.position(), cursor.anchor()); - cursor = bottomOfSelection(); - cursor.movePosition(QTextCursor::NextBlock); + // A reformatList should be called on the block inside selection + // with the lowest indentation level + int minIndentPosition; + int minIndent = -1; - if (cursor.currentList()) { - style = cursor.currentList()->format().style(); + // Changing indentation of all blocks between top and bottom + cursor.setPosition(top); + do { + QTextList *list = cursor.currentList(); + // Setting up listFormat + QTextListFormat listFmt; + if (!list) { + if (delta > 0) { + // No list, we're increasing indentation -> create a new one + listFmt.setStyle(QTextListFormat::ListDisc); + listFmt.setIndent(delta); + } + // else do nothing + } else { + const int newIndent = list->format().indent() + delta; + if (newIndent > 0) { + listFmt = list->format(); + listFmt.setIndent(newIndent); } else { - style = QTextListFormat::ListDisc; + listFmt.setIndent(0); } } - handleOnBulletType(style); - } else { - listFmt = cursor.currentList()->format(); - listFmt.setIndent(listFmt.indent() + 1); - cursor.createList(listFmt); - reformatList(); - } - cursor.endEditBlock(); -} - -void NestedListHelper::handleOnIndentLess() -{ - QTextCursor cursor = textEdit->textCursor(); - QTextList *currentList = cursor.currentList(); - if (!currentList) { - return; - } - cursor.beginEditBlock(); - QTextListFormat listFmt; - listFmt = currentList->format(); - if (listFmt.indent() > 1) { - listFmt.setIndent(listFmt.indent() - 1); - cursor.createList(listFmt); + if (listFmt.indent() > 0) { + // This block belongs to a list: here we create a new one + // for each block, and then let reformatList() sort it out + cursor.createList(listFmt); + if (minIndent == -1 || minIndent > listFmt.indent()) { + minIndent = listFmt.indent(); + minIndentPosition = cursor.block().position(); + } + } else { + // If the block belonged to a list, remove it from there + if (list) { + list->remove(cursor.block()); + } + // The removal does not change the indentation, we need to do it explicitly + QTextBlockFormat blkFmt; + blkFmt.setIndent(0); + cursor.mergeBlockFormat(blkFmt); + } + if (!cursor.block().next().isValid()) { + break; + } + cursor.movePosition(QTextCursor::NextBlock); + } while (cursor.position() < bottom); + // Reformatting the whole list + if (minIndent != -1) { + cursor.setPosition(minIndentPosition); reformatList(cursor.block()); - } else { - QTextBlockFormat bfmt; - bfmt.setObjectIndex(-1); - cursor.setBlockFormat(bfmt); - reformatList(cursor.block().next()); } + cursor.setPosition(top); + reformatList(cursor.block()); cursor.endEditBlock(); } void NestedListHelper::handleOnBulletType(int styleIndex) { QTextCursor cursor = textEdit->textCursor(); if (styleIndex != 0) { QTextListFormat::Style style = static_cast(styleIndex); QTextList *currentList = cursor.currentList(); QTextListFormat listFmt; cursor.beginEditBlock(); if (currentList) { listFmt = currentList->format(); listFmt.setStyle(style); currentList->setFormat(listFmt); } else { listFmt.setStyle(style); cursor.createList(listFmt); } cursor.endEditBlock(); } else { QTextBlockFormat bfmt; bfmt.setObjectIndex(-1); cursor.setBlockFormat(bfmt); } reformatList(); } diff --git a/src/widgets/nestedlisthelper_p.h b/src/widgets/nestedlisthelper_p.h index f3246de..52376a6 100644 --- a/src/widgets/nestedlisthelper_p.h +++ b/src/widgets/nestedlisthelper_p.h @@ -1,133 +1,129 @@ /** * Nested list helper * * Copyright 2008 Stephen Kelly * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301 USA */ #ifndef NESTEDLISTHELPER_H #define NESTEDLISTHELPER_H //@cond PRIVATE class QTextEdit; class QKeyEvent; class QDropEvent; class QTextCursor; class QTextList; class QTextBlock; /** * * @short Helper class for automatic handling of nested lists in a text edit * * * @author Stephen Kelly * @since 4.1 * @internal */ class NestedListHelper { public: /** * Create a helper * * @param te The text edit object to handle lists in. */ explicit NestedListHelper(QTextEdit *te); /** * Destructor */ ~NestedListHelper(); /** * * Handles a key press before it is processed by the text edit widget. * * This includes: * 1. Backspace at the beginning of a line decreases nesting level * 2. Return at the empty list element decreases nesting level * 3. Tab at the beginning of a line OR with a multi-line selection * increases nesting level * * @param event The event to be handled * @return Whether the event was completely handled by this method. */ bool handleKeyPressEvent(QKeyEvent *event); bool handleAfterDropEvent(QDropEvent *event); /** - * Increases the indent (nesting level) on the current list item or selection. + * Changes the indent (nesting level) on a current list item or selection + * by the value @p delta (typically, +1 or -1) */ - void handleOnIndentMore(); - - /** - * Decreases the indent (nesting level) on the current list item or selection. - */ - void handleOnIndentLess(); + void changeIndent(int delta); /** * Changes the style of the current list or creates a new list with * the specified style. * * @param styleIndex The QTextListStyle of the list. */ void handleOnBulletType(int styleIndex); /** * @brief Check whether the current item in the list may be indented. * * An list item must have an item above it on the same or greater level * if it can be indented. * * Also, a block which is currently part of a list can be indented. * * @sa canDedent * * @return Whether the item can be indented. */ bool canIndent() const; /** * \brief Check whether the current item in the list may be dedented. * * An item may be dedented if it is part of a list. * The next item must be at the same or lesser level. * * @sa canIndent * * @return Whether the item can be dedented. */ bool canDedent() const; private: QTextCursor topOfSelection() const; QTextCursor bottomOfSelection() const; void processList(QTextList *list); void reformatList(QTextBlock block); void reformatList(); QTextEdit *textEdit = nullptr; }; //@endcond #endif