diff --git a/autotests/kfindtest.cpp b/autotests/kfindtest.cpp index ba8e101..63a4a0b 100644 --- a/autotests/kfindtest.cpp +++ b/autotests/kfindtest.cpp @@ -1,370 +1,385 @@ /* Copyright (C) 2004, Arend van Beelen jr. Copyright (C) 2010, David Faure This file is part of the KDE project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. 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.LGPL-2. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfindtest.h" +#include #include #include #include void KFindRecorder::changeText(int line, const QString &text) { Q_ASSERT(line < m_text.count()); Q_ASSERT(m_find != nullptr); m_line = line; m_text[line] = text; m_find->setData(line, text); } void KFindRecorder::find(const QString &pattern, long options) { delete m_find; m_find = new KFind(pattern, options, nullptr); // Prevent dialogs from popping up m_find->closeFindNextDialog(); connect(m_find, SIGNAL(highlight(QString,int,int)), SLOT(slotHighlight(QString,int,int))); connect(m_find, SIGNAL(highlight(int,int,int)), SLOT(slotHighlight(int,int,int))); m_line = 0; KFind::Result result = KFind::NoMatch; do { if (options & KFind::FindIncremental) { m_find->setData(m_line, m_text[m_line]); } else { m_find->setData(m_text[m_line]); } m_line++; result = m_find->find(); } while (result == KFind::NoMatch && m_line < m_text.count()); } bool KFindRecorder::findNext(const QString &pattern) { Q_ASSERT(m_find != nullptr); if (!pattern.isNull()) { m_find->setPattern(pattern); } KFind::Result result = KFind::NoMatch; do { //qDebug() << "m_line: " << m_line; result = m_find->find(); if (result == KFind::NoMatch && m_line < m_text.count()) { //qDebug() << "incrementing m_line..."; if (m_find->options() & KFind::FindIncremental) { m_find->setData(m_line, m_text[m_line]); } else { m_find->setData(m_text[m_line]); } m_line++; } } while (result == KFind::NoMatch && m_line < m_text.count()); //qDebug() << "find next completed" << m_line; return result != KFind::NoMatch; } void KFindRecorder::slotHighlight(const QString &text, int index, int matchedLength) { m_hits.append(QLatin1String("line: \"") + text + QLatin1String("\", index: ") + QString::number(index) + QLatin1String(", length: ") + QString::number(matchedLength) + QLatin1Char('\n')); } void KFindRecorder::slotHighlight(int id, int index, int matchedLength) { m_hits.append(QLatin1String("line: \"") + m_text[id] + QLatin1String("\", index: ") + QString::number(index) + QLatin1String(", length: ") + QString::number(matchedLength) + QLatin1Char('\n')); } //// TestKFind::TestKFind() : QObject() { m_text = QLatin1String("This file is part of the KDE project.\n") + QLatin1String("This library is free software; you can redistribute it and/or\n") + QLatin1String("modify it under the terms of the GNU Library General Public\n") + QLatin1String("License version 2, as published by the Free Software Foundation.\n") + QLatin1Char('\n') + QLatin1String(" This library is distributed in the hope that it will be useful,\n") + QLatin1String(" but WITHOUT ANY WARRANTY; without even the implied warranty of\n") + QLatin1String(" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n") + QLatin1String(" Library General Public License for more details.\n") + QLatin1Char('\n') + QLatin1String(" You should have received a copy of the GNU Library General Public License\n") + QLatin1String(" along with this library; see the file COPYING.LIB. If not, write to\n") + QLatin1String(" the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,\n") + QLatin1String(" Boston, MA 02110-1301, USA.\n"); } void TestKFind::testStaticFindString_data() { // Tests for the core method "static KFind::find" QTest::addColumn("text"); QTest::addColumn("pattern"); QTest::addColumn("startIndex"); QTest::addColumn("options"); QTest::addColumn("expectedResult"); QTest::addColumn("expectedMatchedLength"); QTest::newRow("simple (0)") << "abc" << "a" << 0 << 0 << 0 << 1; QTest::newRow("simple (1)") << "abc" << "b" << 0 << 0 << 1 << 1; QTest::newRow("not found") << "abca" << "ba" << 0 << 0 << -1 << 0; QTest::newRow("from index") << "abc bc" << "b" << 3 << 0 << 4 << 1; QTest::newRow("from exact index") << "abc bc" << "b" << 4 << 0 << 4 << 1; QTest::newRow("past index (not found)") << "abc bc" << "b" << 5 << 0 << -1 << 0; QTest::newRow("dot") << "ab." << "b." << 0 << 0 << 1 << 2; QTest::newRow("^should fail") << "text" << "^tex" << 0 << 0 << -1 << 0; QTest::newRow("multiline with \\n") << "foo\nbar" << "o\nb" << 0 << 0 << 2 << 3; QTest::newRow("whole words ok") << "abc bcbc bc bmore be" << "bc" << 0 << int(KFind::WholeWordsOnly) << 9 << 2; QTest::newRow("whole words not found") << "abab abx" << "ab" << 0 << int(KFind::WholeWordsOnly) << -1 << 0; QTest::newRow("whole words not found (_)") << "abab ab_" << "ab" << 0 << int(KFind::WholeWordsOnly) << -1 << 0; QTest::newRow("whole words ok (.)") << "ab." << "ab" << 0 << int(KFind::WholeWordsOnly) << 0 << 2; QTest::newRow("backwards") << "abc bcbc8bc" << "bc" << 10 << int(KFind::FindBackwards) << 9 << 2; QTest::newRow("backwards again") << "abc bcbc8bc" << "bc" << 8 << int(KFind::FindBackwards) << 6 << 2; QTest::newRow("backwards 2") << "abc bcbc8bc" << "bc" << 5 << int(KFind::FindBackwards) << 4 << 2; QTest::newRow("backwards 3") << "abc bcbc8bc" << "bc" << 3 << int(KFind::FindBackwards) << 1 << 2; QTest::newRow("empty (0)") << "a" << "" << 0 << int(0) << 0 << 0; QTest::newRow("empty (1)") << "a" << "" << 1 << int(0) << 1 << 0; // kreplacetest testReplaceBlankSearch relies on this QTest::newRow("at end, not found") << "a" << "b" << 1 << int(0) << -1 << 0; // just for catching the while(index("text"); QTest::addColumn("pattern"); QTest::addColumn("startIndex"); QTest::addColumn("options"); QTest::addColumn("expectedResult"); QTest::addColumn("expectedMatchedLength"); QTest::newRow("simple (0)") << "abc" << "a" << 0 << 0 << 0 << 1; QTest::newRow("simple (1)") << "abc" << "b" << 0 << 0 << 1 << 1; QTest::newRow("not found") << "abca" << "ba" << 0 << 0 << -1 << 0; QTest::newRow("from index") << "abc bc" << "b" << 3 << 0 << 4 << 1; QTest::newRow("from exact index") << "abc bc" << "b" << 4 << 0 << 4 << 1; QTest::newRow("past index (not found)") << "abc bc" << "b" << 5 << 0 << -1 << 0; QTest::newRow("dot") << "abc" << "b." << 0 << 0 << 1 << 2; QTest::newRow("^simple") << "text" << "^tex" << 0 << 0 << 0 << 3; QTest::newRow("^multiline first") << "foo\nbar" << "^f" << 0 << 0 << 0 << 1; QTest::newRow("^multiline last") << "foo\nbar" << "^bar" << 0 << 0 << 4 << 3; QTest::newRow("^multiline with index") << "boo\nbar" << "^b" << 1 << 0 << 4 << 1; QTest::newRow("simple$") << "text" << "xt$" << 0 << 0 << 2 << 2; QTest::newRow("$ backwards") << "text" << "xt$" << 4 << int(KFind::FindBackwards) << 2 << 2; QTest::newRow("multiline$") << "foo\nbar" << "oo$" << 0 << 0 << 1 << 2; QTest::newRow("multiline$ intermediary line") << "foo\nbar\nagain bar" << "r$" << 0 << 0 << 6 << 1; QTest::newRow("multiline$ with index, last line") << "foo\nbar\nagain bar" << "r$" << 7 << 0 << 16 << 1; QTest::newRow("multiline$ backwards") << "foo\nbar" << "oo$" << 7 << int(KFind::FindBackwards) << 1 << 2; QTest::newRow("multiline with \\n") << "foo\nbar" << "o\nb" << 0 << 0 << 2 << 3; QTest::newRow("whole words ok") << "abc bcbc bc bmore be" << "b." << 0 << int(KFind::WholeWordsOnly) << 9 << 2; QTest::newRow("whole words not found") << "abab abx" << "ab" << 0 << int(KFind::WholeWordsOnly) << -1 << 0; QTest::newRow("whole words not found (_)") << "abab ab_" << "ab" << 0 << int(KFind::WholeWordsOnly) << -1 << 0; QTest::newRow("whole words ok (.)") << "ab." << "ab" << 0 << int(KFind::WholeWordsOnly) << 0 << 2; QTest::newRow("backwards") << "abc bcbc bc" << "b." << 10 << int(KFind::FindBackwards) << 9 << 2; QTest::newRow("empty (0)") << "a" << "" << 0 << int(0) << 0 << 0; QTest::newRow("empty (1)") << "a" << "" << 1 << int(0) << 1 << 0; // kreplacetest testReplaceBlankSearch relies on this QTest::newRow("at end, not found") << "a" << "b" << 1 << int(0) << -1 << 0; // just for catching the while(index Copyright (C) 2010, David Faure This file is part of the KDE project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. 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.LGPL-2. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KFINDTEST_H #define KFINDTEST_H #include #include class KFind; class KFindRecorder : public QObject { Q_OBJECT public: KFindRecorder(const QStringList &text) : QObject(nullptr), m_find(nullptr), m_text(text), m_line(0) {} void find(const QString &pattern, long options = 0); bool findNext(const QString &pattern = QString()); void changeText(int line, const QString &text); const QStringList &hits() const { return m_hits; } void clearHits() { m_hits.clear(); } public Q_SLOTS: void slotHighlight(const QString &text, int index, int matchedLength); void slotHighlight(int id, int index, int matchedLengthlength); private: KFind *m_find; QStringList m_text; int m_line; QStringList m_hits; }; class TestKFind : public QObject { Q_OBJECT public: TestKFind(); private Q_SLOTS: void testStaticFindString_data(); void testStaticFindString(); void testStaticFindRegexp_data(); void testStaticFindRegexp(); void testSimpleSearch(); void testSimpleRegexp(); void testLineBeginRegexp(); + void testLineBeginRegularExpression(); void testFindIncremental(); void testFindIncrementalDynamic(); private: QString m_text; }; #endif diff --git a/autotests/krichtextedittest.cpp b/autotests/krichtextedittest.cpp index 9b733af..94daf97 100644 --- a/autotests/krichtextedittest.cpp +++ b/autotests/krichtextedittest.cpp @@ -1,247 +1,253 @@ /* 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 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("" ) target_include_directories(KF5TextWidgets INTERFACE "$") target_link_libraries(KF5TextWidgets PUBLIC Qt5::Widgets KF5::SonnetUi KF5::I18n PRIVATE KF5::SonnetCore KF5::ConfigWidgets KF5::Completion ) if (Qt5TextToSpeech_FOUND) target_link_libraries(KF5TextWidgets PRIVATE Qt5::TextToSpeech) endif() set_target_properties(KF5TextWidgets PROPERTIES VERSION ${KTEXTWIDGETS_VERSION_STRING} SOVERSION ${KTEXTWIDGETS_SOVERSION} EXPORT_NAME TextWidgets ) ecm_generate_headers(KTextWidgets_HEADERS HEADER_NAMES KRichTextEdit KRichTextWidget KTextEdit KPluralHandlingSpinBox RELATIVE widgets REQUIRED_HEADERS KTextWidgets_HEADERS ) ecm_generate_headers(KTextWidgets_HEADERS HEADER_NAMES KFind KFindDialog KReplace KReplaceDialog RELATIVE findreplace REQUIRED_HEADERS KTextWidgets_HEADERS ) ecm_generate_headers(KTextWidgets_HEADERS HEADER_NAMES KRegExpEditorInterface RELATIVE kregexpeditor REQUIRED_HEADERS KTextWidgets_HEADERS ) install(TARGETS KF5TextWidgets EXPORT KF5TextWidgetsTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/ktextwidgets_export.h ${KTextWidgets_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KTextWidgets COMPONENT Devel ) if (BUILD_DESIGNERPLUGIN) add_subdirectory(designer) endif() if (BUILD_QCH) ecm_add_qch( KF5TextWidgets_QCH NAME KTextWidgets BASE_NAME KF5TextWidgets VERSION ${KF5_VERSION} ORG_DOMAIN org.kde SOURCES # using only public headers, to cover only public API ${KTextWidgets_HEADERS} MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" IMAGE_DIRS "${CMAKE_SOURCE_DIR}/docs/pics" LINK_QCHS Qt5Widgets_QCH KF5SonnetUi_QCH KF5I18n_QCH INCLUDE_DIRS ${CMAKE_CURRENT_BINARY_DIR} ${ktextwidgets_INCLUDES} BLANK_MACROS KTEXTWIDGETS_EXPORT KTEXTWIDGETS_DEPRECATED_EXPORT KTEXTWIDGETS_DEPRECATED "KTEXTWIDGETS_DEPRECATED_VERSION(x, y, t)" TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KTextWidgets LIB_NAME KF5TextWidgets DEPS "widgets SonnetUi KI18n" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KTextWidgets) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/findreplace/kfind.cpp b/src/findreplace/kfind.cpp index 031ea13..5ca8d18 100644 --- a/src/findreplace/kfind.cpp +++ b/src/findreplace/kfind.cpp @@ -1,746 +1,794 @@ /* Copyright (C) 2001, S.R.Haque . Copyright (C) 2002, David Faure Copyright (C) 2004, Arend van Beelen jr. This file is part of the KDE project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. 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.LGPL-2. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfind.h" #include "kfind_p.h" #include "kfinddialog.h" #include #include #include #include #include #include #include #include +#include #include #include // #define DEBUG_FIND static const int INDEX_NOMATCH = -1; class KFindNextDialog : public QDialog { Q_OBJECT public: explicit KFindNextDialog(const QString &pattern, QWidget *parent); QPushButton *findButton() const; private: QPushButton *m_findButton = nullptr; }; // Create the dialog. KFindNextDialog::KFindNextDialog(const QString &pattern, QWidget *parent) : QDialog(parent) { setModal(false); setWindowTitle(i18n("Find Next")); QVBoxLayout *layout = new QVBoxLayout(this); layout->addWidget(new QLabel(i18n("Find next occurrence of '%1'?", pattern), this)); m_findButton = new QPushButton; KGuiItem::assign(m_findButton, KStandardGuiItem::find()); m_findButton->setDefault(true); QDialogButtonBox *buttonBox = new QDialogButtonBox(this); buttonBox->addButton(m_findButton, QDialogButtonBox::ActionRole); buttonBox->setStandardButtons(QDialogButtonBox::Close); layout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); } QPushButton *KFindNextDialog::findButton() const { return m_findButton; } //// KFind::KFind(const QString &pattern, long options, QWidget *parent) : QObject(parent), d(new KFind::Private(this)) { d->options = options; d->init(pattern); } KFind::KFind(const QString &pattern, long options, QWidget *parent, QWidget *findDialog) : QObject(parent), d(new KFind::Private(this)) { d->findDialog = findDialog; d->options = options; d->init(pattern); } void KFind::Private::init(const QString &_pattern) { matches = 0; pattern = _pattern; dialog = nullptr; dialogClosed = false; index = INDEX_NOMATCH; lastResult = NoMatch; - regExp = nullptr; - q->setOptions(options); // create d->regExp with the right options + +#if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70) + regExp = nullptr; // QRegExp + +#endif + // TODO: KF6 change this comment once d->regExp is removed + // set options and create d->regExp with the right options + q->setOptions(options); } KFind::~KFind() { delete d; } bool KFind::needData() const { // always true when d->text is empty. if (d->options & KFind::FindBackwards) // d->index==-1 and d->lastResult==Match means we haven't answered nomatch yet // This is important in the "replace with a prompt" case. { return (d->index < 0 && d->lastResult != Match); } else // "index over length" test removed: we want to get a nomatch before we set data again // This is important in the "replace with a prompt" case. { return d->index == INDEX_NOMATCH; } } void KFind::setData(const QString &data, int startPos) { setData(-1, data, startPos); } void KFind::setData(int id, const QString &data, int startPos) { // cache the data for incremental find if (d->options & KFind::FindIncremental) { if (id != -1) { d->customIds = true; } else { id = d->currentId + 1; } Q_ASSERT(id <= d->data.size()); if (id == d->data.size()) { d->data.append(Private::Data(id, data, true)); } else { d->data.replace(id, Private::Data(id, data, true)); } Q_ASSERT(d->data.at(id).text == data); } if (!(d->options & KFind::FindIncremental) || needData()) { d->text = data; if (startPos != -1) { d->index = startPos; } else if (d->options & KFind::FindBackwards) { d->index = d->text.length(); } else { d->index = 0; } #ifdef DEBUG_FIND //qDebug() << "setData: '" << d->text << "' d->index=" << d->index; #endif Q_ASSERT(d->index != INDEX_NOMATCH); d->lastResult = NoMatch; d->currentId = id; } } QDialog *KFind::findNextDialog(bool create) { if (!d->dialog && create) { KFindNextDialog *dialog = new KFindNextDialog(d->pattern, parentWidget()); connect(dialog->findButton(), SIGNAL(clicked()), this, SLOT(_k_slotFindNext())); connect(dialog, SIGNAL(finished(int)), this, SLOT(_k_slotDialogClosed())); d->dialog = dialog; } return d->dialog; } KFind::Result KFind::find() { Q_ASSERT(d->index != INDEX_NOMATCH || d->patternChanged); if (d->lastResult == Match && !d->patternChanged) { // Move on before looking for the next match, _if_ we just found a match if (d->options & KFind::FindBackwards) { d->index--; if (d->index == -1) { // don't call KFind::find with -1, it has a special meaning d->lastResult = NoMatch; return NoMatch; } } else { d->index++; } } d->patternChanged = false; if (d->options & KFind::FindIncremental) { // if the current pattern is shorter than the matchedPattern we can // probably look up the match in the incrementalPath if (d->pattern.length() < d->matchedPattern.length()) { Private::Match match; if (!d->pattern.isEmpty()) { match = d->incrementalPath.value(d->pattern); } else if (d->emptyMatch) { match = *d->emptyMatch; } QString previousPattern(d->matchedPattern); d->matchedPattern = d->pattern; if (!match.isNull()) { bool clean = true; // find the first result backwards on the path that isn't dirty while (d->data.at(match.dataId).dirty == true && !d->pattern.isEmpty()) { d->pattern.truncate(d->pattern.length() - 1); match = d->incrementalPath.value(d->pattern); clean = false; } // remove all matches that lie after the current match while (d->pattern.length() < previousPattern.length()) { d->incrementalPath.remove(previousPattern); previousPattern.truncate(previousPattern.length() - 1); } // set the current text, index, etc. to the found match d->text = d->data.at(match.dataId).text; d->index = match.index; d->matchedLength = match.matchedLength; d->currentId = match.dataId; // if the result is clean we can return it now if (clean) { if (d->customIds) { emit highlight(d->currentId, d->index, d->matchedLength); } else { emit highlight(d->text, d->index, d->matchedLength); } d->lastResult = Match; d->matchedPattern = d->pattern; return Match; } } // if we couldn't look up the match, the new pattern isn't a // substring of the matchedPattern, so we start a new search else { d->startNewIncrementalSearch(); } } // if the new pattern is longer than the matchedPattern we might be // able to proceed from the last search else if (d->pattern.length() > d->matchedPattern.length()) { // continue from the previous pattern if (d->pattern.startsWith(d->matchedPattern)) { // we can't proceed from the previous position if the previous // position already failed if (d->index == INDEX_NOMATCH) { return NoMatch; } QString temp(d->pattern); d->pattern.truncate(d->matchedPattern.length() + 1); d->matchedPattern = temp; } // start a new search else { d->startNewIncrementalSearch(); } } // if the new pattern is as long as the matchedPattern, we reset if // they are not equal else if (d->pattern != d->matchedPattern) { d->startNewIncrementalSearch(); } } #ifdef DEBUG_FIND //qDebug() << "d->index=" << d->index; #endif do { // if we have multiple data blocks in our cache, walk through these // blocks till we either searched all blocks or we find a match do { // Find the next candidate match. - if (d->options & KFind::RegularExpression) { - d->index = KFind::find(d->text, *d->regExp, d->index, d->options, &d->matchedLength); - } else { - d->index = KFind::find(d->text, d->pattern, d->index, d->options, &d->matchedLength); - } + d->index = KFind::find(d->text, d->pattern, d->index, d->options, &d->matchedLength, nullptr); if (d->options & KFind::FindIncremental) { d->data[d->currentId].dirty = false; } if (d->index == -1 && d->currentId < d->data.count() - 1) { d->text = d->data.at(++d->currentId).text; if (d->options & KFind::FindBackwards) { d->index = d->text.length(); } else { d->index = 0; } } else { break; } } while (!(d->options & KFind::RegularExpression)); if (d->index != -1) { // Flexibility: the app can add more rules to validate a possible match if (validateMatch(d->text, d->index, d->matchedLength)) { bool done = true; if (d->options & KFind::FindIncremental) { if (d->pattern.isEmpty()) { delete d->emptyMatch; d->emptyMatch = new Private::Match(d->currentId, d->index, d->matchedLength); } else { d->incrementalPath.insert(d->pattern, Private::Match(d->currentId, d->index, d->matchedLength)); } if (d->pattern.length() < d->matchedPattern.length()) { d->pattern += d->matchedPattern.midRef(d->pattern.length(), 1); done = false; } } if (done) { d->matches++; // Tell the world about the match we found, in case someone wants to // highlight it. if (d->customIds) { emit highlight(d->currentId, d->index, d->matchedLength); } else { emit highlight(d->text, d->index, d->matchedLength); } if (!d->dialogClosed) { findNextDialog(true)->show(); } #ifdef DEBUG_FIND //qDebug() << "Match. Next d->index=" << d->index; #endif d->lastResult = Match; return Match; } } else { // Skip match if (d->options & KFind::FindBackwards) { d->index--; } else { d->index++; } } } else { if (d->options & KFind::FindIncremental) { QString temp(d->pattern); temp.truncate(temp.length() - 1); d->pattern = d->matchedPattern; d->matchedPattern = temp; } d->index = INDEX_NOMATCH; } } while (d->index != INDEX_NOMATCH); #ifdef DEBUG_FIND //qDebug() << "NoMatch. d->index=" << d->index; #endif d->lastResult = NoMatch; return NoMatch; } void KFind::Private::startNewIncrementalSearch() { Private::Match *match = emptyMatch; if (match == nullptr) { text.clear(); index = 0; currentId = 0; } else { text = data.at(match->dataId).text; index = match->index; currentId = match->dataId; } matchedLength = 0; incrementalPath.clear(); delete emptyMatch; emptyMatch = nullptr; matchedPattern = pattern; pattern.clear(); } static bool isInWord(QChar ch) { return ch.isLetter() || ch.isDigit() || ch == QLatin1Char('_'); } static bool isWholeWords(const QString &text, int starts, int matchedLength) { if (starts == 0 || !isInWord(text.at(starts - 1))) { const int ends = starts + matchedLength; if (ends == text.length() || !isInWord(text.at(ends))) { return true; } } return false; } static bool matchOk(const QString &text, int index, int matchedLength, long options) { if (options & KFind::WholeWordsOnly) { // Is the match delimited correctly? if (isWholeWords(text, index, matchedLength)) { return true; } } else { // Non-whole-word search: this match is good return true; } return false; } +static int findRegex(const QString &text, const QString &pattern, int index, long options, + int *matchedLength, QRegularExpressionMatch *rmatch) +{ + QRegularExpression::PatternOptions opts; + // instead of this rudimentary test, add a checkbox to toggle MultilineOption ? + if (pattern.startsWith(QLatin1Char('^')) || pattern.endsWith(QLatin1Char('$'))) { + opts |= QRegularExpression::MultilineOption; + } + + opts |= (options & KFind::CaseSensitive) ? QRegularExpression::NoPatternOption + : QRegularExpression::CaseInsensitiveOption; + + QRegularExpression re(pattern, opts); + QRegularExpressionMatch match; + if (options & KFind::FindBackwards) { + // Backward search, until the beginning of the line... + text.lastIndexOf(re, index, &match); + } else { + // Forward search, until the end of the line... + match = re.match(text, index); + } + + // index is -1 if no match is found + index = match.capturedStart(0); + // matchedLength is 0 if no match is found + *matchedLength = match.capturedLength(0); + + if (rmatch) { + *rmatch = match; + } + + return index; +} + // static int KFind::find(const QString &text, const QString &pattern, int index, long options, int *matchedLength) +{ + return find(text, pattern, index, options, matchedLength, nullptr); +} + +// static +int KFind::find(const QString &text, const QString &pattern, int index, long options, + int *matchedLength, QRegularExpressionMatch *rmatch) { // Handle regular expressions in the appropriate way. if (options & KFind::RegularExpression) { - Qt::CaseSensitivity caseSensitive = (options & KFind::CaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive; - QRegExp regExp(pattern, caseSensitive); - - return find(text, regExp, index, options, matchedLength); + return findRegex(text, pattern, index, options, matchedLength, rmatch); } // In Qt4 QString("aaaaaa").lastIndexOf("a",6) returns -1; we need // to start at text.length() - pattern.length() to give a valid index to QString. if (options & KFind::FindBackwards) { index = qMin(qMax(0, text.length() - pattern.length()), index); } Qt::CaseSensitivity caseSensitive = (options & KFind::CaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive; if (options & KFind::FindBackwards) { // Backward search, until the beginning of the line... while (index >= 0) { // ...find the next match. index = text.lastIndexOf(pattern, index, caseSensitive); if (index == -1) { break; } if (matchOk(text, index, pattern.length(), options)) { break; } index--; //qDebug() << "decrementing:" << index; } } else { // Forward search, until the end of the line... while (index <= text.length()) { // ...find the next match. index = text.indexOf(pattern, index, caseSensitive); if (index == -1) { break; } if (matchOk(text, index, pattern.length(), options)) { break; } index++; } if (index > text.length()) { // end of line //qDebug() << "at" << index << "-> not found"; index = -1; // not found } } if (index <= -1) { *matchedLength = 0; } else { *matchedLength = pattern.length(); } return index; } +#if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70) // Core method for the regexp-based find static int doFind(const QString &text, const QRegExp &pattern, int index, long options, int *matchedLength) { if (options & KFind::FindBackwards) { // Backward search, until the beginning of the line... while (index >= 0) { // ...find the next match. index = text.lastIndexOf(pattern, index); if (index == -1) { break; } /*int pos =*/ pattern.indexIn(text.mid(index)); *matchedLength = pattern.matchedLength(); if (matchOk(text, index, *matchedLength, options)) { break; } index--; } } else { // Forward search, until the end of the line... while (index <= text.length()) { // ...find the next match. index = text.indexOf(pattern, index); if (index == -1) { break; } /*int pos =*/ pattern.indexIn(text.mid(index)); *matchedLength = pattern.matchedLength(); if (matchOk(text, index, *matchedLength, options)) { break; } index++; } if (index > text.length()) { // end of line index = -1; // not found } } if (index == -1) { *matchedLength = 0; } return index; } // Since QRegExp doesn't support multiline searches (the equivalent of perl's /m) // we have to cut the text into lines if the pattern starts with ^ or ends with $. static int lineBasedFind(const QString &text, const QRegExp &pattern, int index, long options, int *matchedLength) { const QStringList lines = text.split(QLatin1Char('\n')); int offset = 0; // Use "index" to find the first line we should start from int startLineNumber = 0; for (; startLineNumber < lines.count(); ++startLineNumber) { const QString line = lines.at(startLineNumber); if (index < offset + line.length()) { break; } offset += line.length() + 1 /*newline*/; } if (options & KFind::FindBackwards) { if (startLineNumber == lines.count()) { // We went too far, go back to the last line --startLineNumber; offset -= lines.at(startLineNumber).length() + 1; } for (int lineNumber = startLineNumber; lineNumber >= 0; --lineNumber) { const QString line = lines.at(lineNumber); const int ret = doFind(line, pattern, lineNumber == startLineNumber ? index - offset : line.length(), options, matchedLength); if (ret > -1) { return ret + offset; } offset -= line.length() + 1 /*newline*/; } } else { for (int lineNumber = startLineNumber; lineNumber < lines.count(); ++lineNumber) { const QString line = lines.at(lineNumber); const int ret = doFind(line, pattern, lineNumber == startLineNumber ? (index - offset) : 0, options, matchedLength); if (ret > -1) { return ret + offset; } offset += line.length() + 1 /*newline*/; } } return -1; } +#endif // static int KFind::find(const QString &text, const QRegExp &pattern, int index, long options, int *matchedLength) { if (pattern.pattern().startsWith(QLatin1Char('^')) || pattern.pattern().endsWith(QLatin1Char('$'))) { return lineBasedFind(text, pattern, index, options, matchedLength); } return doFind(text, pattern, index, options, matchedLength); } void KFind::Private::_k_slotFindNext() { emit q->findNext(); } void KFind::Private::_k_slotDialogClosed() { #ifdef DEBUG_FIND //qDebug() << " Begin"; #endif emit q->dialogClosed(); dialogClosed = true; #ifdef DEBUG_FIND //qDebug() << " End"; #endif } void KFind::displayFinalDialog() const { QString message; if (numMatches()) { message = i18np("1 match found.", "%1 matches found.", numMatches()); } else { message = i18n("No matches found for '%1'.", d->pattern.toHtmlEscaped()); } KMessageBox::information(dialogsParent(), message); } bool KFind::shouldRestart(bool forceAsking, bool showNumMatches) const { // Only ask if we did a "find from cursor", otherwise it's pointless. // Well, unless the user can modify the document during a search operation, // hence the force boolean. if (!forceAsking && (d->options & KFind::FromCursor) == 0) { displayFinalDialog(); return false; } QString message; if (showNumMatches) { if (numMatches()) { message = i18np("1 match found.", "%1 matches found.", numMatches()); } else { message = i18n("No matches found for '%1'.", d->pattern.toHtmlEscaped()); } } else { if (d->options & KFind::FindBackwards) { message = i18n("Beginning of document reached."); } else { message = i18n("End of document reached."); } } message += QLatin1String("

        "); // can't be in the i18n() of the first if() because of the plural form. // Hope this word puzzle is ok, it's a different sentence message += (d->options & KFind::FindBackwards) ? i18n("Continue from the end?") : i18n("Continue from the beginning?"); int ret = KMessageBox::questionYesNo(dialogsParent(), QStringLiteral("%1").arg(message), QString(), KStandardGuiItem::cont(), KStandardGuiItem::stop()); bool yes = (ret == KMessageBox::Yes); if (yes) { const_cast(this)->d->options &= ~KFind::FromCursor; // clear FromCursor option } return yes; } long KFind::options() const { return d->options; } void KFind::setOptions(long options) { d->options = options; +#if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70) delete d->regExp; if (d->options & KFind::RegularExpression) { Qt::CaseSensitivity caseSensitive = (d->options & KFind::CaseSensitive) ? Qt::CaseSensitive : Qt::CaseInsensitive; d->regExp = new QRegExp(d->pattern, caseSensitive); } else { d->regExp = nullptr; } +#endif } void KFind::closeFindNextDialog() { if (d->dialog) { d->dialog->deleteLater(); d->dialog = nullptr; } d->dialogClosed = true; } int KFind::index() const { return d->index; } QString KFind::pattern() const { return d->pattern; } void KFind::setPattern(const QString &pattern) { if (d->pattern != pattern) { d->patternChanged = true; d->matches = 0; } d->pattern = pattern; - setOptions(options()); // rebuild d->regExp if necessary + + // TODO: KF6 change this comment once d->regExp is removed + // set the options and rebuild d->regeExp if necessary + setOptions(options()); } int KFind::numMatches() const { return d->matches; } void KFind::resetCounts() { d->matches = 0; } bool KFind::validateMatch(const QString &, int, int) { return true; } QWidget *KFind::parentWidget() const { return static_cast(parent()); } QWidget *KFind::dialogsParent() const { // If the find dialog is still up, it should get the focus when closing a message box // Otherwise, maybe the "find next?" dialog is up // Otherwise, the "view" is the parent. return d->findDialog ? static_cast(d->findDialog) : (d->dialog ? d->dialog : parentWidget()); } #include "moc_kfind.cpp" #include "kfind.moc" diff --git a/src/findreplace/kfind.h b/src/findreplace/kfind.h index e985622..3f5678a 100644 --- a/src/findreplace/kfind.h +++ b/src/findreplace/kfind.h @@ -1,376 +1,409 @@ /* Copyright (C) 2001, S.R.Haque . Copyright (C) 2002, David Faure This file is part of the KDE project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. 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.LGPL-2. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KFIND_H #define KFIND_H #include "ktextwidgets_export.h" #include class QDialog; /** * @class KFind kfind.h * * @brief A generic implementation of the "find" function. * * @author S.R.Haque , David Faure , * Arend van Beelen jr. * * \b Detail: * * This class includes prompt handling etc. Also provides some * static functions which can be used to create custom behavior * instead of using the class directly. * * \b Example: * * To use the class to implement a complete find feature: * * In the slot connected to the find action, after using KFindDialog: * \code * * // This creates a find-next-prompt dialog if needed. * m_find = new KFind(pattern, options, this); * * // Connect highlight signal to code which handles highlighting * // of found text. * connect(m_find, SIGNAL(highlight(QString, int, int)), * this, SLOT(slotHighlight(QString, int, int))); * // Connect findNext signal - called when pressing the button in the dialog * connect(m_find, SIGNAL(findNext()), * this, SLOT(slotFindNext())); * \endcode * * Then initialize the variables determining the "current position" * (to the cursor, if the option FromCursor is set, * to the beginning of the selection if the option SelectedText is set, * and to the beginning of the document otherwise). * Initialize the "end of search" variables as well (end of doc or end of selection). * Swap begin and end if FindBackwards. * Finally, call slotFindNext(); * * \code * void slotFindNext() * { * KFind::Result res = KFind::NoMatch; * while (res == KFind::NoMatch && ) { * if (m_find->needData()) * m_find->setData(); * * // Let KFind inspect the text fragment, and display a dialog if a match is found * res = m_find->find(); * * if (res == KFind::NoMatch) { * * } * } * * if (res == KFind::NoMatch) // i.e. at end * displayFinalDialog(); m_find->deleteLater(); m_find = nullptr; * or if (m_find->shouldRestart()) { reinit (w/o FromCursor); m_find->resetCounts(); slotFindNext(); } * else { m_find->closeFindNextDialog(); }> * } * \endcode * * Don't forget to delete m_find in the destructor of your class, * unless you gave it a parent widget on construction. * * This implementation allows to have a "Find Next" action, which resumes the * search, even if the user closed the "Find Next" dialog. * * A "Find Previous" action can simply switch temporarily the value of * FindBackwards and call slotFindNext() - and reset the value afterwards. */ class KTEXTWIDGETS_EXPORT KFind : public QObject { Q_OBJECT public: /// the options enum Options { WholeWordsOnly = 1, ///< Match whole words only. FromCursor = 2, ///< Start from current cursor position. SelectedText = 4, ///< Only search selected area. CaseSensitive = 8, ///< Consider case when matching. FindBackwards = 16, ///< Go backwards. RegularExpression = 32, ///< Interpret the pattern as a regular expression. FindIncremental = 64, ///< Find incremental. // Note that KReplaceDialog uses 256 and 512 // User extensions can use boolean options above this value. MinimumUserOption = 65536 ///< user options start with this bit }; Q_DECLARE_FLAGS(SearchOptions, Options) /** * Only use this constructor if you don't use KFindDialog, or if * you use it as a modal dialog. */ KFind(const QString &pattern, long options, QWidget *parent); /** * This is the recommended constructor if you also use KFindDialog (non-modal). * You should pass the pointer to it here, so that when a message box * appears it has the right parent. Don't worry about deletion, KFind * will notice if the find dialog is closed. */ KFind(const QString &pattern, long options, QWidget *parent, QWidget *findDialog); virtual ~KFind(); enum Result { NoMatch, Match }; /** * @return true if the application must supply a new text fragment * It also means the last call returned "NoMatch". But by storing this here * the application doesn't have to store it in a member variable (between * calls to slotFindNext()). */ bool needData() const; /** * Call this when needData returns true, before calling find(). * @param data the text fragment (line) * @param startPos if set, the index at which the search should start. * This is only necessary for the very first call to setData usually, * for the 'find in selection' feature. A value of -1 (the default value) * means "process all the data", i.e. either 0 or data.length()-1 depending * on FindBackwards. */ void setData(const QString &data, int startPos = -1); /** * Call this when needData returns true, before calling find(). The use of * ID's is especially useful if you're using the FindIncremental option. * @param id the id of the text fragment * @param data the text fragment (line) * @param startPos if set, the index at which the search should start. * This is only necessary for the very first call to setData usually, * for the 'find in selection' feature. A value of -1 (the default value) * means "process all the data", i.e. either 0 or data.length()-1 depending * on FindBackwards. */ void setData(int id, const QString &data, int startPos = -1); /** * Walk the text fragment (e.g. text-processor line, kspread cell) looking for matches. * For each match, emits the highlight() signal and displays the find-again dialog * proceeding. */ Result find(); /** * Return the current options. * * Warning: this is usually the same value as the one passed to the constructor, * but options might change _during_ the replace operation: * e.g. the "All" button resets the PromptOnReplace flag. * * @see KFind::Options */ long options() const; /** * Set new options. Usually this is used for setting or clearing the * FindBackwards options. * * @see KFind::Options */ virtual void setOptions(long options); /** * @return the pattern we're currently looking for */ QString pattern() const; /** * Change the pattern we're looking for */ void setPattern(const QString &pattern); /** * Return the number of matches found (i.e. the number of times * the highlight signal was emitted). * If 0, can be used in a dialog box to tell the user "no match was found". * The final dialog does so already, unless you used setDisplayFinalDialog(false). */ int numMatches() const; /** * Call this to reset the numMatches count * (and the numReplacements count for a KReplace). * Can be useful if reusing the same KReplace for different operations, * or when restarting from the beginning of the document. */ virtual void resetCounts(); /** * Virtual method, which allows applications to add extra checks for * validating a candidate match. It's only necessary to reimplement this * if the find dialog extension has been used to provide additional * criterias. * * @param text The current text fragment * @param index The starting index where the candidate match was found * @param matchedlength The length of the candidate match */ virtual bool validateMatch(const QString &text, int index, int matchedlength); /** * Returns true if we should restart the search from scratch. * Can ask the user, or return false (if we already searched the whole document). * * @param forceAsking set to true if the user modified the document during the * search. In that case it makes sense to restart the search again. * * @param showNumMatches set to true if the dialog should show the number of * matches. Set to false if the application provides a "find previous" action, * in which case the match count will be erroneous when hitting the end, * and we could even be hitting the beginning of the document (so not all * matches have even been seen). */ virtual bool shouldRestart(bool forceAsking = false, bool showNumMatches = true) const; +#if KTEXTWIDGETS_ENABLE_DEPRECATED_SINCE(5, 70) /** * Search the given string, and returns whether a match was found. If one is, * the length of the string matched is also returned. * * A performance optimised version of the function is provided for use * with regular expressions. * * @param text The string to search. * @param pattern The pattern to look for. * @param index The starting index into the string. * @param options The options to use. * @param matchedlength The length of the string that was matched * @return The index at which a match was found, or -1 if no match was found. + * + * @deprecated Since 5.70 */ + + KTEXTWIDGETS_DEPRECATED_VERSION(5, 70, "Use find(const QString &text, const QString &pattern, int index, long options, \ + int *matchedLength, QRegularExpressionMatch *rmatch).") + static int find(const QString &text, const QString &pattern, int index, long options, int *matchedlength); + /** @deprecated Since 5.70, for lack of direct use + */ static int find(const QString &text, const QRegExp &pattern, int index, long options, int *matchedlength); +#endif + + /** + * Search @p text for @p pattern. If a match is found, the length of the matched + * string will be stored in @p matchedLength and the index of the matched string + * will be returned. If no match is found -1 is returned. + * + * If the KFind::RegularExpression flag is set, the @p pattern will be iterpreted + * as a regular expression (using QRegularExpression). + * + * @param text The string to search in + * @param pattern The pattern to search for + * @param index The index in @p text from which to start the search + * @param options The options to use + * @param matchedlength If there is a match, its length will be stored in this parameter + * @param rmatch If there is a regular expression match (implies that the KFind::RegularExpression + * flag is set) and @p rmatch is not a nullptr the match result will be stored + * in this QRegularExpressionMatch object + * @return The index at which a match was found otherwise -1 + * + * @since 5.70 + */ + static int find(const QString &text, const QString &pattern, int index, long options, + int *matchedLength, QRegularExpressionMatch *rmatch); /** * Displays the final dialog saying "no match was found", if that was the case. * Call either this or shouldRestart(). */ virtual void displayFinalDialog() const; /** * Return (or create) the dialog that shows the "find next?" prompt. * Usually you don't need to call this. * One case where it can be useful, is when the user selects the "Find" * menu item while a find operation is under way. In that case, the * program may want to call setActiveWindow() on that dialog. */ QDialog *findNextDialog(bool create = false); /** * Close the "find next?" dialog. The application should do this when * the last match was hit. If the application deletes the KFind, then * "find previous" won't be possible anymore. * * IMPORTANT: you should also call this if you are using a non-modal * find dialog, to tell KFind not to pop up its own dialog. */ void closeFindNextDialog(); /** * @return the current matching index (or -1). * Same as the matchingIndex parameter passed to highlight. * You usually don't need to use this, except maybe when updating the current data, * so you need to call setData(newData, index()). */ int index() const; Q_SIGNALS: /** * Connect to this signal to implement highlighting of found text during the find * operation. * * If you've set data with setData(id, text), use the signal highlight(id, * matchingIndex, matchedLength) * * WARNING: If you're using the FindIncremental option, the text argument * passed by this signal is not necessarily the data last set through * setData(), but can also be an earlier set data block. * * @see setData() */ void highlight(const QString &text, int matchingIndex, int matchedLength); /** * Connect to this signal to implement highlighting of found text during the find * operation. * * Use this signal if you've set your data with setData(id, text), otherwise * use the signal with highlight(text, matchingIndex, matchedLength). * * WARNING: If you're using the FindIncremental option, the id argument * passed by this signal is not necessarily the id of the data last set * through setData(), but can also be of an earlier set data block. * * @see setData() */ void highlight(int id, int matchingIndex, int matchedLength); // ## TODO docu // findprevious will also emit findNext, after temporarily switching the value // of FindBackwards void findNext(); /** * Emitted when the options have changed. * This can happen e.g. with "Replace All", or if our 'find next' dialog * gets a "find previous" one day. */ void optionsChanged(); /** * Emitted when the 'find next' dialog is being closed. * Some apps might want to remove the highlighted text when this happens. * Apps without support for "Find Next" can also do m_find->deleteLater() * to terminate the find operation. */ void dialogClosed(); protected: QWidget *parentWidget() const; QWidget *dialogsParent() const; private: friend class KReplace; friend class KReplacePrivate; struct Private; Private *const d; Q_PRIVATE_SLOT(d, void _k_slotFindNext()) Q_PRIVATE_SLOT(d, void _k_slotDialogClosed()) }; Q_DECLARE_OPERATORS_FOR_FLAGS(KFind::SearchOptions) #endif diff --git a/src/findreplace/kfinddialog.cpp b/src/findreplace/kfinddialog.cpp index 4ec55a1..4bb2b8e 100644 --- a/src/findreplace/kfinddialog.cpp +++ b/src/findreplace/kfinddialog.cpp @@ -1,589 +1,586 @@ /* Copyright (C) 2001, S.R.Haque . Copyright (C) 2002, David Faure This file is part of the KDE project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. 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.LGPL-2. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kfinddialog.h" #include "kfinddialog_p.h" #include "kfind.h" #include #include #include #include #include #include #include -#include +#include #include #include #include #include #include #include KFindDialog::KFindDialog(QWidget *parent, long options, const QStringList &findStrings, bool hasSelection, bool replaceDialog) : QDialog(parent), d(new KFindDialogPrivate(this)) { setWindowTitle(i18n("Find Text")); d->init(replaceDialog, findStrings, hasSelection); setOptions(options); } KFindDialog::~KFindDialog() { delete d; } QWidget *KFindDialog::findExtension() const { if (!d->findExtension) { d->findExtension = new QWidget(d->findGrp); d->findLayout->addWidget(d->findExtension, 3, 0, 1, 2); } return d->findExtension; } QStringList KFindDialog::findHistory() const { return d->find->historyItems(); } void KFindDialog::KFindDialogPrivate::init(bool forReplace, const QStringList &_findStrings, bool hasSelection) { QVBoxLayout *topLayout; QGridLayout *optionsLayout; // Create common parts of dialog. topLayout = new QVBoxLayout; q->setLayout(topLayout); findGrp = new QGroupBox(i18nc("@title:group", "Find"), q); findLayout = new QGridLayout(findGrp); QLabel *findLabel = new QLabel(i18n("&Text to find:"), findGrp); find = new KHistoryComboBox(findGrp); find->setMaxCount(10); find->setDuplicatesEnabled(false); regExp = new QCheckBox(i18n("Regular e&xpression"), findGrp); regExpItem = new QPushButton(i18n("&Edit..."), findGrp); regExpItem->setEnabled(false); findLayout->addWidget(findLabel, 0, 0); findLayout->addWidget(find, 1, 0, 1, 2); findLayout->addWidget(regExp, 2, 0); findLayout->addWidget(regExpItem, 2, 1); topLayout->addWidget(findGrp); replaceGrp = new QGroupBox(i18n("Replace With"), q); replaceLayout = new QGridLayout(replaceGrp); QLabel *replaceLabel = new QLabel(i18n("Replace&ment text:"), replaceGrp); replace = new KHistoryComboBox(replaceGrp); replace->setMaxCount(10); replace->setDuplicatesEnabled(false); backRef = new QCheckBox(i18n("Use p&laceholders"), replaceGrp); backRefItem = new QPushButton(i18n("Insert Place&holder"), replaceGrp); backRefItem->setEnabled(false); replaceLayout->addWidget(replaceLabel, 0, 0); replaceLayout->addWidget(replace, 1, 0, 1, 2); replaceLayout->addWidget(backRef, 2, 0); replaceLayout->addWidget(backRefItem, 2, 1); topLayout->addWidget(replaceGrp); QGroupBox *optionGrp = new QGroupBox(i18n("Options"), q); optionsLayout = new QGridLayout(optionGrp); caseSensitive = new QCheckBox(i18n("C&ase sensitive"), optionGrp); wholeWordsOnly = new QCheckBox(i18n("&Whole words only"), optionGrp); fromCursor = new QCheckBox(i18n("From c&ursor"), optionGrp); findBackwards = new QCheckBox(i18n("Find &backwards"), optionGrp); selectedText = new QCheckBox(i18n("&Selected text"), optionGrp); q->setHasSelection(hasSelection); // If we have a selection, we make 'find in selection' default // and if we don't, then the option has to be unchecked, obviously. selectedText->setChecked(hasSelection); _k_slotSelectedTextToggled(hasSelection); promptOnReplace = new QCheckBox(i18n("&Prompt on replace"), optionGrp); promptOnReplace->setChecked(true); optionsLayout->addWidget(caseSensitive, 0, 0); optionsLayout->addWidget(wholeWordsOnly, 1, 0); optionsLayout->addWidget(fromCursor, 2, 0); optionsLayout->addWidget(findBackwards, 0, 1); optionsLayout->addWidget(selectedText, 1, 1); optionsLayout->addWidget(promptOnReplace, 2, 1); topLayout->addWidget(optionGrp); buttonBox = new QDialogButtonBox(q); buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Close); q->connect(buttonBox, SIGNAL(accepted()), q, SLOT(_k_slotOk())); q->connect(buttonBox, SIGNAL(rejected()), q, SLOT(_k_slotReject())); topLayout->addWidget(buttonBox); // We delay creation of these until needed. patterns = nullptr; placeholders = nullptr; // signals and slots connections q->connect(selectedText, SIGNAL(toggled(bool)), q, SLOT(_k_slotSelectedTextToggled(bool))); q->connect(regExp, &QAbstractButton::toggled, regExpItem, &QWidget::setEnabled); q->connect(backRef, &QAbstractButton::toggled, backRefItem, &QWidget::setEnabled); q->connect(regExpItem, SIGNAL(clicked()), q, SLOT(_k_showPatterns())); q->connect(backRefItem, SIGNAL(clicked()), q, SLOT(_k_showPlaceholders())); q->connect(find, SIGNAL(editTextChanged(QString)), q, SLOT(_k_textSearchChanged(QString))); q->connect(regExp, &QAbstractButton::toggled, q, &KFindDialog::optionsChanged); q->connect(backRef, &QAbstractButton::toggled, q, &KFindDialog::optionsChanged); q->connect(caseSensitive, &QAbstractButton::toggled, q, &KFindDialog::optionsChanged); q->connect(wholeWordsOnly, &QAbstractButton::toggled, q, &KFindDialog::optionsChanged); q->connect(fromCursor, &QAbstractButton::toggled, q, &KFindDialog::optionsChanged); q->connect(findBackwards, &QAbstractButton::toggled, q, &KFindDialog::optionsChanged); q->connect(selectedText, &QAbstractButton::toggled, q, &KFindDialog::optionsChanged); q->connect(promptOnReplace, &QAbstractButton::toggled, q, &KFindDialog::optionsChanged); // tab order q->setTabOrder(find, regExp); q->setTabOrder(regExp, regExpItem); q->setTabOrder(regExpItem, replace); //findExtension widgets are inserted in showEvent() q->setTabOrder(replace, backRef); q->setTabOrder(backRef, backRefItem); q->setTabOrder(backRefItem, caseSensitive); q->setTabOrder(caseSensitive, wholeWordsOnly); q->setTabOrder(wholeWordsOnly, fromCursor); q->setTabOrder(fromCursor, findBackwards); q->setTabOrder(findBackwards, selectedText); q->setTabOrder(selectedText, promptOnReplace); // buddies findLabel->setBuddy(find); replaceLabel->setBuddy(replace); if (!forReplace) { promptOnReplace->hide(); replaceGrp->hide(); } findStrings = _findStrings; find->setFocus(); QPushButton *buttonOk = buttonBox->button(QDialogButtonBox::Ok); buttonOk->setEnabled(!q->pattern().isEmpty()); if (forReplace) { KGuiItem::assign(buttonOk, KGuiItem(i18n("&Replace"), QString(), i18n("Start replace"), i18n("If you press the Replace button, the text you entered " "above is searched for within the document and any occurrence is " "replaced with the replacement text."))); } else { KGuiItem::assign(buttonOk, KGuiItem(i18n("&Find"), QStringLiteral("edit-find"), i18n("Start searching"), i18n("If you press the Find button, the text you entered " "above is searched for within the document."))); } // QWhatsthis texts find->setWhatsThis(i18n( "Enter a pattern to search for, or select a previous pattern from " "the list.")); regExp->setWhatsThis(i18n( "If enabled, search for a regular expression.")); regExpItem->setWhatsThis(i18n( "Click here to edit your regular expression using a graphical editor.")); replace->setWhatsThis(i18n( "Enter a replacement string, or select a previous one from the list.")); backRef->setWhatsThis(i18n( "If enabled, any occurrence of \\N, where " "N is an integer number, will be replaced with " "the corresponding capture (\"parenthesized substring\") from the " "pattern.

        To include (a literal \\N in your " "replacement, put an extra backslash in front of it, like " "\\\\N.

        ")); backRefItem->setWhatsThis(i18n( "Click for a menu of available captures.")); wholeWordsOnly->setWhatsThis(i18n( "Require word boundaries in both ends of a match to succeed.")); fromCursor->setWhatsThis(i18n( "Start searching at the current cursor location rather than at the top.")); selectedText->setWhatsThis(i18n( "Only search within the current selection.")); caseSensitive->setWhatsThis(i18n( "Perform a case sensitive search: entering the pattern " "'Joe' will not match 'joe' or 'JOE', only 'Joe'.")); findBackwards->setWhatsThis(i18n( "Search backwards.")); promptOnReplace->setWhatsThis(i18n( "Ask before replacing each match found.")); _k_textSearchChanged(find->lineEdit()->text()); } void KFindDialog::KFindDialogPrivate::_k_textSearchChanged(const QString &text) { buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!text.isEmpty()); } void KFindDialog::showEvent(QShowEvent *e) { if (!d->initialShowDone) { d->initialShowDone = true; // only once //qDebug() << "showEvent\n"; if (!d->findStrings.isEmpty()) { setFindHistory(d->findStrings); } d->findStrings = QStringList(); if (!d->pattern.isEmpty()) { d->find->lineEdit()->setText(d->pattern); d->find->lineEdit()->selectAll(); d->pattern.clear(); } //maintain a user-friendly tab order if (d->findExtension) { QWidget *prev = d->regExpItem; const auto children = d->findExtension->findChildren(); for (QWidget *child : children) { setTabOrder(prev, child); prev = child; } setTabOrder(prev, d->replace); } } d->find->setFocus(); QDialog::showEvent(e); } long KFindDialog::options() const { long options = 0; if (d->caseSensitive->isChecked()) { options |= KFind::CaseSensitive; } if (d->wholeWordsOnly->isChecked()) { options |= KFind::WholeWordsOnly; } if (d->fromCursor->isChecked()) { options |= KFind::FromCursor; } if (d->findBackwards->isChecked()) { options |= KFind::FindBackwards; } if (d->selectedText->isChecked()) { options |= KFind::SelectedText; } if (d->regExp->isChecked()) { options |= KFind::RegularExpression; } return options; } QString KFindDialog::pattern() const { return d->find->currentText(); } void KFindDialog::setPattern(const QString &pattern) { d->find->lineEdit()->setText(pattern); d->find->lineEdit()->selectAll(); d->pattern = pattern; //qDebug() << "setPattern " << pattern; } void KFindDialog::setFindHistory(const QStringList &strings) { if (!strings.isEmpty()) { d->find->setHistoryItems(strings, true); d->find->lineEdit()->setText(strings.first()); d->find->lineEdit()->selectAll(); } else { d->find->clearHistory(); } } void KFindDialog::setHasSelection(bool hasSelection) { if (hasSelection) { d->enabled |= KFind::SelectedText; } else { d->enabled &= ~KFind::SelectedText; } d->selectedText->setEnabled(hasSelection); if (!hasSelection) { d->selectedText->setChecked(false); d->_k_slotSelectedTextToggled(hasSelection); } } void KFindDialog::KFindDialogPrivate::_k_slotSelectedTextToggled(bool selec) { // From cursor doesn't make sense if we have a selection fromCursor->setEnabled(!selec && (enabled & KFind::FromCursor)); if (selec) { // uncheck if disabled fromCursor->setChecked(false); } } void KFindDialog::setHasCursor(bool hasCursor) { if (hasCursor) { d->enabled |= KFind::FromCursor; } else { d->enabled &= ~KFind::FromCursor; } d->fromCursor->setEnabled(hasCursor); d->fromCursor->setChecked(hasCursor && (options() & KFind::FromCursor)); } void KFindDialog::setSupportsBackwardsFind(bool supports) { // ########## Shouldn't this hide the checkbox instead? if (supports) { d->enabled |= KFind::FindBackwards; } else { d->enabled &= ~KFind::FindBackwards; } d->findBackwards->setEnabled(supports); d->findBackwards->setChecked(supports && (options() & KFind::FindBackwards)); } void KFindDialog::setSupportsCaseSensitiveFind(bool supports) { // ########## This should hide the checkbox instead if (supports) { d->enabled |= KFind::CaseSensitive; } else { d->enabled &= ~KFind::CaseSensitive; } d->caseSensitive->setEnabled(supports); d->caseSensitive->setChecked(supports && (options() & KFind::CaseSensitive)); } void KFindDialog::setSupportsWholeWordsFind(bool supports) { // ########## This should hide the checkbox instead if (supports) { d->enabled |= KFind::WholeWordsOnly; } else { d->enabled &= ~KFind::WholeWordsOnly; } d->wholeWordsOnly->setEnabled(supports); d->wholeWordsOnly->setChecked(supports && (options() & KFind::WholeWordsOnly)); } void KFindDialog::setSupportsRegularExpressionFind(bool supports) { if (supports) { d->enabled |= KFind::RegularExpression; } else { d->enabled &= ~KFind::RegularExpression; } d->regExp->setEnabled(supports); d->regExp->setChecked(supports && (options() & KFind::RegularExpression)); if (!supports) { d->regExpItem->hide(); d->regExp->hide(); } else { d->regExpItem->show(); d->regExp->show(); } } void KFindDialog::setOptions(long options) { d->caseSensitive->setChecked((d->enabled & KFind::CaseSensitive) && (options & KFind::CaseSensitive)); d->wholeWordsOnly->setChecked((d->enabled & KFind::WholeWordsOnly) && (options & KFind::WholeWordsOnly)); d->fromCursor->setChecked((d->enabled & KFind::FromCursor) && (options & KFind::FromCursor)); d->findBackwards->setChecked((d->enabled & KFind::FindBackwards) && (options & KFind::FindBackwards)); d->selectedText->setChecked((d->enabled & KFind::SelectedText) && (options & KFind::SelectedText)); d->regExp->setChecked((d->enabled & KFind::RegularExpression) && (options & KFind::RegularExpression)); } // Create a popup menu with a list of regular expression terms, to help the user // compose a regular expression search pattern. void KFindDialog::KFindDialogPrivate::_k_showPatterns() { typedef struct { const char *description; const char *regExp; int cursorAdjustment; } Term; static const Term items[] = { { I18N_NOOP("Any Character"), ".", 0 }, { I18N_NOOP("Start of Line"), "^", 0 }, { I18N_NOOP("End of Line"), "$", 0 }, { I18N_NOOP("Set of Characters"), "[]", -1 }, { I18N_NOOP("Repeats, Zero or More Times"), "*", 0 }, { I18N_NOOP("Repeats, One or More Times"), "+", 0 }, { I18N_NOOP("Optional"), "?", 0 }, { I18N_NOOP("Escape"), "\\", 0 }, { I18N_NOOP("TAB"), "\\t", 0 }, { I18N_NOOP("Newline"), "\\n", 0 }, { I18N_NOOP("Carriage Return"), "\\r", 0 }, { I18N_NOOP("White Space"), "\\s", 0 }, { I18N_NOOP("Digit"), "\\d", 0 }, }; class RegExpAction : public QAction { public: RegExpAction(QObject *parent, const QString &text, const QString ®Exp, int cursor) : QAction(text, parent), mText(text), mRegExp(regExp), mCursor(cursor) { } QString text() const { return mText; } QString regExp() const { return mRegExp; } int cursor() const { return mCursor; } private: QString mText; QString mRegExp; int mCursor; }; // Populate the popup menu. if (!patterns) { patterns = new QMenu(q); for (const Term &item : items) { patterns->addAction(new RegExpAction(patterns, i18n(item.description), QLatin1String(item.regExp), item.cursorAdjustment)); } } // Insert the selection into the edit control. QAction *action = patterns->exec(regExpItem->mapToGlobal(regExpItem->rect().bottomLeft())); if (action) { RegExpAction *regExpAction = static_cast(action); if (regExpAction) { QLineEdit *editor = find->lineEdit(); editor->insert(regExpAction->regExp()); editor->setCursorPosition(editor->cursorPosition() + regExpAction->cursor()); } } } class PlaceHolderAction : public QAction { public: PlaceHolderAction(QObject *parent, const QString &text, int id) : QAction(text, parent), mText(text), mId(id) { } QString text() const { return mText; } int id() const { return mId; } private: QString mText; int mId; }; // Create a popup menu with a list of backreference terms, to help the user // compose a regular expression replacement pattern. void KFindDialog::KFindDialogPrivate::_k_showPlaceholders() { // Populate the popup menu. if (!placeholders) { placeholders = new QMenu(q); q->connect(placeholders, SIGNAL(aboutToShow()), q, SLOT(_k_slotPlaceholdersAboutToShow())); } // Insert the selection into the edit control. QAction *action = placeholders->exec(backRefItem->mapToGlobal(backRefItem->rect().bottomLeft())); if (action) { PlaceHolderAction *placeHolderAction = static_cast(action); if (placeHolderAction) { QLineEdit *editor = replace->lineEdit(); editor->insert(QStringLiteral("\\%1").arg(placeHolderAction->id())); } } } void KFindDialog::KFindDialogPrivate::_k_slotPlaceholdersAboutToShow() { placeholders->clear(); placeholders->addAction(new PlaceHolderAction(placeholders, i18n("Complete Match"), 0)); - QRegExp r(q->pattern()); - int n = r.captureCount(); - for (int i = 0; i < n; i++) { - placeholders->addAction(new PlaceHolderAction(placeholders, i18n("Captured Text (%1)", i + 1), i + 1)); + const int n = QRegularExpression(q->pattern()).captureCount(); + for (int i = 1; i <= n; ++i) { + placeholders->addAction(new PlaceHolderAction(placeholders, i18n("Captured Text (%1)", i), i)); } } void KFindDialog::KFindDialogPrivate::_k_slotOk() { // Nothing to find? if (q->pattern().isEmpty()) { KMessageBox::error(q, i18n("You must enter some text to search for.")); return; } if (regExp->isChecked()) { // Check for a valid regular expression. - QRegExp _regExp(q->pattern()); - - if (!_regExp.isValid()) { - KMessageBox::error(q, i18n("Invalid regular expression.")); + if (!QRegularExpression(q->pattern()).isValid()) { + KMessageBox::error(q, i18n("Invalid PCRE pattern syntax.")); return; } } find->addToHistory(q->pattern()); if (q->windowModality() != Qt::NonModal) { q->accept(); } emit q->okClicked(); } void KFindDialog::KFindDialogPrivate::_k_slotReject() { emit q->cancelClicked(); q->reject(); } #include "moc_kfinddialog.cpp" diff --git a/src/findreplace/kreplace.cpp b/src/findreplace/kreplace.cpp index c5f7d09..a2c64bc 100644 --- a/src/findreplace/kreplace.cpp +++ b/src/findreplace/kreplace.cpp @@ -1,412 +1,444 @@ /* Copyright (C) 2001, S.R.Haque . Copyright (C) 2002, David Faure This file is part of the KDE project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. 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.LGPL-2. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kreplace.h" #include "kfind_p.h" #include "kreplacedialog.h" #include #include #include #include #include +#include #include #include //#define DEBUG_REPLACE #define INDEX_NOMATCH -1 class KReplaceNextDialog : public QDialog { Q_OBJECT public: explicit KReplaceNextDialog(QWidget *parent); void setLabel(const QString &pattern, const QString &replacement); QPushButton *replaceAllButton() const; QPushButton *skipButton() const; QPushButton *replaceButton() const; private: QLabel *m_mainLabel = nullptr; QPushButton *m_allButton = nullptr; QPushButton *m_skipButton = nullptr; QPushButton *m_replaceButton = nullptr; }; KReplaceNextDialog::KReplaceNextDialog(QWidget *parent) : QDialog(parent) { setModal(false); setWindowTitle(i18n("Replace")); QVBoxLayout *layout = new QVBoxLayout; setLayout(layout); m_mainLabel = new QLabel(this); layout->addWidget(m_mainLabel); m_allButton = new QPushButton(i18nc("@action:button Replace all occurrences", "&All")); m_allButton->setObjectName(QStringLiteral("allButton")); m_skipButton = new QPushButton(i18n("&Skip")); m_skipButton->setObjectName(QStringLiteral("skipButton")); m_replaceButton = new QPushButton(i18n("Replace")); m_replaceButton->setObjectName(QStringLiteral("replaceButton")); m_replaceButton->setDefault(true); QDialogButtonBox *buttonBox = new QDialogButtonBox(this); buttonBox->addButton(m_allButton, QDialogButtonBox::ActionRole); buttonBox->addButton(m_skipButton, QDialogButtonBox::ActionRole); buttonBox->addButton(m_replaceButton, QDialogButtonBox::ActionRole); buttonBox->setStandardButtons(QDialogButtonBox::Close); layout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); } void KReplaceNextDialog::setLabel(const QString &pattern, const QString &replacement) { m_mainLabel->setText(i18n("Replace '%1' with '%2'?", pattern, replacement)); } QPushButton *KReplaceNextDialog::replaceAllButton() const { return m_allButton; } QPushButton *KReplaceNextDialog::skipButton() const { return m_skipButton; } QPushButton *KReplaceNextDialog::replaceButton() const { return m_replaceButton; } //// class KReplacePrivate { public: KReplacePrivate(KReplace *q, const QString &replacement) : q(q) , m_replacement(replacement) {} KReplaceNextDialog *dialog(); void doReplace(); void _k_slotSkip(); void _k_slotReplace(); void _k_slotReplaceAll(); KReplace *q; QString m_replacement; int m_replacements = 0; + QRegularExpressionMatch m_match; }; //// KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent) : KFind(pattern, options, parent), d(new KReplacePrivate(this, replacement)) { } KReplace::KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent, QWidget *dlg) : KFind(pattern, options, parent, dlg), d(new KReplacePrivate(this, replacement)) { } KReplace::~KReplace() { delete d; } int KReplace::numReplacements() const { return d->m_replacements; } QDialog *KReplace::replaceNextDialog(bool create) { if (KFind::d->dialog || create) { return d->dialog(); } return nullptr; } KReplaceNextDialog *KReplacePrivate::dialog() { if (!q->KFind::d->dialog) { KReplaceNextDialog *dialog = new KReplaceNextDialog(q->parentWidget()); q->connect(dialog->replaceAllButton(), SIGNAL(clicked()), q, SLOT(_k_slotReplaceAll())); q->connect(dialog->skipButton(), SIGNAL(clicked()), q, SLOT(_k_slotSkip())); q->connect(dialog->replaceButton(), SIGNAL(clicked()), q, SLOT(_k_slotReplace())); q->connect(dialog, SIGNAL(finished(int)), q, SLOT(_k_slotDialogClosed())); q->KFind::d->dialog = dialog; } return static_cast(q->KFind::d->dialog); } void KReplace::displayFinalDialog() const { if (!d->m_replacements) { KMessageBox::information(parentWidget(), i18n("No text was replaced.")); } else { KMessageBox::information(parentWidget(), i18np("1 replacement done.", "%1 replacements done.", d->m_replacements)); } } +#if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70) static int replaceHelper(QString &text, const QString &replacement, int index, long options, int length, const QRegExp *regExp) { QString rep(replacement); if (options & KReplaceDialog::BackReference) { // Backreferences: replace \0 with the right portion of 'text' rep.replace(QLatin1String("\\0"), text.mid(index, length)); // Other backrefs if (regExp) { const QStringList caps = regExp->capturedTexts(); for (int i = 0; i < caps.count(); ++i) { rep.replace(QLatin1String("\\") + QString::number(i), caps.at(i)); } } } // Then replace rep into the text text.replace(index, length, rep); return rep.length(); } +#endif + +static int replaceHelper(QString &text, const QString &replacement, int index, long options, + const QRegularExpressionMatch *match, int length) +{ + QString rep(replacement); + if (options & KReplaceDialog::BackReference) { + // Handle backreferences + if (options & KFind::RegularExpression) { // regex search + Q_ASSERT(match); + const int capNum = match->regularExpression().captureCount(); + for (int i = 0 ; i <= capNum; ++i) { + rep.replace(QLatin1String("\\") + QString::number(i), match->captured(i)); + } + } else { // with non-regex search only \0 is supported, replace it with the + // right portion of 'text' + rep.replace(QLatin1String("\\0"), text.mid(index, length)); + } + } + + // Then replace rep into the text + text.replace(index, length, rep); + return rep.length(); +} KFind::Result KReplace::replace() { KFind::Private *df = KFind::d; #ifdef DEBUG_REPLACE //qDebug() << "d->index=" << df->index; #endif if (df->index == INDEX_NOMATCH && df->lastResult == Match) { df->lastResult = NoMatch; return NoMatch; } do { // this loop is only because validateMatch can fail #ifdef DEBUG_REPLACE //qDebug() << "beginning of loop: df->index=" << df->index; #endif // Find the next match. - if (df->options & KFind::RegularExpression) { - df->index = KFind::find(df->text, *df->regExp, df->index, df->options, &df->matchedLength); - } else { - df->index = KFind::find(df->text, df->pattern, df->index, df->options, &df->matchedLength); - } + df->index = KFind::find(df->text, df->pattern, df->index, df->options, &df->matchedLength, + df->options & KFind::RegularExpression? &d->m_match : nullptr); #ifdef DEBUG_REPLACE //qDebug() << "KFind::find returned df->index=" << df->index; #endif if (df->index != -1) { // Flexibility: the app can add more rules to validate a possible match if (validateMatch(df->text, df->index, df->matchedLength)) { if (df->options & KReplaceDialog::PromptOnReplace) { #ifdef DEBUG_REPLACE //qDebug() << "PromptOnReplace"; #endif // Display accurate initial string and replacement string, they can vary QString matchedText(df->text.mid(df->index, df->matchedLength)); QString rep(matchedText); - replaceHelper(rep, d->m_replacement, 0, df->options, df->matchedLength, df->regExp); + replaceHelper(rep, d->m_replacement, 0, df->options, + df->options & KFind::RegularExpression ? &d->m_match : nullptr, + df->matchedLength); d->dialog()->setLabel(matchedText, rep); d->dialog()->show(); // TODO kde5: virtual void showReplaceNextDialog(QString,QString), so that kreplacetest can skip the show() // Tell the world about the match we found, in case someone wants to // highlight it. emit highlight(df->text, df->index, df->matchedLength); df->lastResult = Match; return Match; } else { d->doReplace(); // this moves on too } } else { // not validated -> move on if (df->options & KFind::FindBackwards) { df->index--; } else { df->index++; } } } else { df->index = INDEX_NOMATCH; // will exit the loop } } while (df->index != INDEX_NOMATCH); df->lastResult = NoMatch; return NoMatch; } int KReplace::replace(QString &text, const QString &pattern, const QString &replacement, int index, long options, int *replacedLength) { int matchedLength; + QRegularExpressionMatch match; + index = KFind::find(text, pattern, index, options, &matchedLength, + options & KFind::RegularExpression ? &match : nullptr); - index = KFind::find(text, pattern, index, options, &matchedLength); if (index != -1) { - *replacedLength = replaceHelper(text, replacement, index, options, matchedLength, nullptr); + *replacedLength = replaceHelper(text, replacement, index, options, + options & KFind::RegularExpression ? &match : nullptr, + matchedLength); if (options & KFind::FindBackwards) { index--; } else { index += *replacedLength; } } return index; } +#if KTEXTWIDGETS_BUILD_DEPRECATED_SINCE(5, 70) int KReplace::replace(QString &text, const QRegExp &pattern, const QString &replacement, int index, long options, int *replacedLength) { int matchedLength; index = KFind::find(text, pattern, index, options, &matchedLength); if (index != -1) { *replacedLength = replaceHelper(text, replacement, index, options, matchedLength, &pattern); if (options & KFind::FindBackwards) { index--; } else { index += *replacedLength; } } return index; } +#endif void KReplacePrivate::_k_slotReplaceAll() { doReplace(); q->KFind::d->options &= ~KReplaceDialog::PromptOnReplace; emit q->optionsChanged(); emit q->findNext(); } void KReplacePrivate::_k_slotSkip() { if (q->KFind::d->options & KFind::FindBackwards) { q->KFind::d->index--; } else { q->KFind::d->index++; } if (q->KFind::d->dialogClosed) { q->KFind::d->dialog->deleteLater(); q->KFind::d->dialog = nullptr; // hide it again } else { emit q->findNext(); } } void KReplacePrivate::_k_slotReplace() { doReplace(); if (q->KFind::d->dialogClosed) { q->KFind::d->dialog->deleteLater(); q->KFind::d->dialog = nullptr; // hide it again } else { emit q->findNext(); } } void KReplacePrivate::doReplace() { KFind::Private *df = q->KFind::d; Q_ASSERT(df->index >= 0); - const int replacedLength = replaceHelper(df->text, m_replacement, df->index, df->options, df->matchedLength, df->regExp); + const int replacedLength = replaceHelper(df->text, m_replacement, df->index, df->options, &m_match, df->matchedLength); // Tell the world about the replacement we made, in case someone wants to // highlight it. emit q->replace(df->text, df->index, replacedLength, df->matchedLength); #ifdef DEBUG_REPLACE //qDebug() << "after replace() signal: KFind::d->index=" << df->index << " replacedLength=" << replacedLength; #endif m_replacements++; if (df->options & KFind::FindBackwards) { Q_ASSERT(df->index >= 0); df->index--; } else { df->index += replacedLength; // when replacing the empty pattern, move on. See also kjs/regexp.cpp for how this should be done for regexps. if (df->pattern.isEmpty()) { ++(df->index); } } #ifdef DEBUG_REPLACE //qDebug() << "after adjustement: KFind::d->index=" << df->index; #endif } void KReplace::resetCounts() { KFind::resetCounts(); d->m_replacements = 0; } bool KReplace::shouldRestart(bool forceAsking, bool showNumMatches) const { // Only ask if we did a "find from cursor", otherwise it's pointless. // ... Or if the prompt-on-replace option was set. // Well, unless the user can modify the document during a search operation, // hence the force boolean. if (!forceAsking && (KFind::d->options & KFind::FromCursor) == 0 && (KFind::d->options & KReplaceDialog::PromptOnReplace) == 0) { displayFinalDialog(); return false; } QString message; if (showNumMatches) { if (!d->m_replacements) { message = i18n("No text was replaced."); } else { message = i18np("1 replacement done.", "%1 replacements done.", d->m_replacements); } } else { if (KFind::d->options & KFind::FindBackwards) { message = i18n("Beginning of document reached."); } else { message = i18n("End of document reached."); } } message += QLatin1Char('\n'); // Hope this word puzzle is ok, it's a different sentence message += (KFind::d->options & KFind::FindBackwards) ? i18n("Do you want to restart search from the end?") : i18n("Do you want to restart search at the beginning?"); int ret = KMessageBox::questionYesNo(parentWidget(), message, QString(), KGuiItem(i18nc("@action:button Restart find & replace", "Restart")), KGuiItem(i18nc("@action:button Stop find & replace", "Stop"))); return (ret == KMessageBox::Yes); } void KReplace::closeReplaceNextDialog() { closeFindNextDialog(); } #include "moc_kreplace.cpp" #include "kreplace.moc" diff --git a/src/findreplace/kreplace.h b/src/findreplace/kreplace.h index f2c23d4..b8795e1 100644 --- a/src/findreplace/kreplace.h +++ b/src/findreplace/kreplace.h @@ -1,227 +1,239 @@ /* Copyright (C) 2001, S.R.Haque . Copyright (C) 2002, David Faure This file is part of the KDE project This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License version 2, as published by the Free Software Foundation. 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.LGPL-2. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef KREPLACE_H #define KREPLACE_H #include "kfind.h" #include "ktextwidgets_export.h" class KReplacePrivate; /** * @class KReplace kreplace.h * * @brief A generic implementation of the "replace" function. * * @author S.R.Haque , David Faure * * \b Detail: * * This class includes prompt handling etc. Also provides some * static functions which can be used to create custom behavior * instead of using the class directly. * * \b Example: * * To use the class to implement a complete replace feature: * * In the slot connect to the replace action, after using KReplaceDialog: * \code * * // This creates a replace-on-prompt dialog if needed. * m_replace = new KReplace(pattern, replacement, options, this); * * // Connect signals to code which handles highlighting * // of found text, and on-the-fly replacement. * connect( m_replace, SIGNAL( highlight( const QString &, int, int ) ), * this, SLOT( slotHighlight( const QString &, int, int ) ) ); * // Connect findNext signal - called when pressing the button in the dialog * connect( m_replace, SIGNAL( findNext() ), * this, SLOT( slotReplaceNext() ) ); * // Connect replace signal - called when doing a replacement * connect( m_replace, SIGNAL( replace(const QString &, int, int, int) ), * this, SLOT( slotReplace(const QString &, int, int, int) ) ); * \endcode * Then initialize the variables determining the "current position" * (to the cursor, if the option FromCursor is set, * to the beginning of the selection if the option SelectedText is set, * and to the beginning of the document otherwise). * Initialize the "end of search" variables as well (end of doc or end of selection). * Swap begin and end if FindBackwards. * Finally, call slotReplaceNext(); * * \code * void slotReplaceNext() * { * KFind::Result res = KFind::NoMatch; * while ( res == KFind::NoMatch && ) { * if ( m_replace->needData() ) * m_replace->setData( ); * * // Let KReplace inspect the text fragment, and display a dialog if a match is found * res = m_replace->replace(); * * if ( res == KFind::NoMatch ) { * * } * } * * if ( res == KFind::NoMatch ) // i.e. at end * displayFinalDialog(); delete m_replace; m_replace = nullptr; * or if ( m_replace->shouldRestart() ) { reinit (w/o FromCursor) and call slotReplaceNext(); } * else { m_replace->closeReplaceNextDialog(); }> * } * \endcode * * Don't forget delete m_find in the destructor of your class, * unless you gave it a parent widget on construction. * */ class KTEXTWIDGETS_EXPORT KReplace : public KFind { Q_OBJECT public: /** * Only use this constructor if you don't use KFindDialog, or if * you use it as a modal dialog. */ KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent = nullptr); /** * This is the recommended constructor if you also use KReplaceDialog (non-modal). * You should pass the pointer to it here, so that when a message box * appears it has the right parent. Don't worry about deletion, KReplace * will notice if the find dialog is closed. */ KReplace(const QString &pattern, const QString &replacement, long options, QWidget *parent, QWidget *replaceDialog); ~KReplace() override; /** * Return the number of replacements made (i.e. the number of times * the replace signal was emitted). * Can be used in a dialog box to tell the user how many replacements were made. * The final dialog does so already, unless you used setDisplayFinalDialog(false). */ int numReplacements() const; /** * Call this to reset the numMatches & numReplacements counts. * Can be useful if reusing the same KReplace for different operations, * or when restarting from the beginning of the document. */ void resetCounts() override; /** * Walk the text fragment (e.g. kwrite line, kspread cell) looking for matches. * For each match, if prompt-on-replace is specified, emits the highlight() signal * and displays the prompt-for-replace dialog before doing the replace. */ Result replace(); /** * Return (or create) the dialog that shows the "find next?" prompt. * Usually you don't need to call this. * One case where it can be useful, is when the user selects the "Find" * menu item while a find operation is under way. In that case, the * program may want to call setActiveWindow() on that dialog. */ QDialog *replaceNextDialog(bool create = false); /** * Close the "replace next?" dialog. The application should do this when * the last match was hit. If the application deletes the KReplace, then * "find previous" won't be possible anymore. */ void closeReplaceNextDialog(); /** - * Search the given string, replaces with the given replacement string, - * and returns whether a match was found. If one is, - * the replacement string length is also returned. + * Searches the given @p text for @p pattern; if a match is found it is replaced + * with @p replacement and the index of the replacement string is returned. * - * A performance optimised version of the function is provided for use - * with regular expressions. + * @param text The string to search + * @param pattern The pattern to search for + * @param replacement The replacement string to insert into the text + * @param index The starting index into the string + * @param options The options to use + * @param replacedLength Output parameter, contains the length of the replaced string + * Not always the same as replacement.length(), when backreferences are used + * @return The index at which a match was found, or -1 otherwise + */ + static int replace(QString &text, const QString &pattern, const QString &replacement, int index, long options, int *replacedLength); + +#if KTEXTWIDGETS_ENABLE_DEPRECATED_SINCE(5, 70) + /** + * This is an overloaded function provided for use with QRegExp regular expressions. * * @param text The string to search. - * @param pattern The pattern to look for. + * @param pattern The QRegExp object used for searching. * @param replacement The replacement string to insert into the text. * @param index The starting index into the string. * @param options The options to use. * @param replacedLength Output parameter, contains the length of the replaced string. * Not always the same as replacement.length(), when backreferences are used. * @return The index at which a match was found, or -1 if no match was found. - */ - static int replace(QString &text, const QString &pattern, const QString &replacement, int index, long options, int *replacedLength); + * @deprecated Since 5.70, for lack of direct use + */ static int replace(QString &text, const QRegExp &pattern, const QString &replacement, int index, long options, int *replacedLength); +#endif /** * Returns true if we should restart the search from scratch. * Can ask the user, or return false (if we already searched/replaced the * whole document without the PromptOnReplace option). * * @param forceAsking set to true if the user modified the document during the * search. In that case it makes sense to restart the search again. * * @param showNumMatches set to true if the dialog should show the number of * matches. Set to false if the application provides a "find previous" action, * in which case the match count will be erroneous when hitting the end, * and we could even be hitting the beginning of the document (so not all * matches have even been seen). */ bool shouldRestart(bool forceAsking = false, bool showNumMatches = true) const override; /** * Displays the final dialog telling the user how many replacements were made. * Call either this or shouldRestart(). */ void displayFinalDialog() const override; Q_SIGNALS: /** * Connect to this slot to implement updating of replaced text during the replace * operation. * * Extra care must be taken to properly implement the "no prompt-on-replace" case. * For instance highlight isn't emitted in that case (some code might rely on it), * and for performance reasons one should repaint after replace() ONLY if * prompt-on-replace was selected. * * @param text The text, in which the replacement has already been done. * @param replacementIndex Starting index of the matched substring * @param replacedLength Length of the replacement string * @param matchedLength Length of the matched string */ void replace(const QString &text, int replacementIndex, int replacedLength, int matchedLength); private: friend class KReplacePrivate; KReplacePrivate *const d; Q_PRIVATE_SLOT(d, void _k_slotSkip()) Q_PRIVATE_SLOT(d, void _k_slotReplace()) Q_PRIVATE_SLOT(d, void _k_slotReplaceAll()) }; #endif diff --git a/src/widgets/krichtextedit.cpp b/src/widgets/krichtextedit.cpp index 4e214c0..220d671 100644 --- a/src/widgets/krichtextedit.cpp +++ b/src/widgets/krichtextedit.cpp @@ -1,585 +1,574 @@ /* * 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->activateRichText(); } void KRichTextEdit::indentListLess() { d->nestedListHelper->handleOnIndentLess(); } void KRichTextEdit::insertHorizontalRule() { QTextCursor cursor = textCursor(); QTextBlockFormat bf = cursor.blockFormat(); QTextCharFormat cf = cursor.charFormat(); cursor.beginEditBlock(); cursor.insertHtml(QStringLiteral("
        ")); cursor.insertBlock(bf, cf); setTextCursor(cursor); d->activateRichText(); cursor.endEditBlock(); } 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::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 is False if the key press event was not handled or not completely // handled by the Helper class. handled = d->nestedListHelper->handleBeforeKeyPressEvent(event); } if (!handled) { KTextEdit::keyPressEvent(event); } if (textCursor().currentList()) { d->nestedListHelper->handleAfterKeyPressEvent(event); } 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

           

          . - - 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); - } + // 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; }