From 80afe26873d5355537d90a144bec23e03fa78fe8 Mon Sep 17 00:00:00 2001 From: Nicolas Fella Date: Fri, 8 Apr 2022 20:05:37 +0200 Subject: [PATCH] Implement event.change This represents the newly entered data for each keystroke. This is often a single added character, but for cases like pasting text it can be more complex. The PDF API reference doesn't specify any algorithm to use. The algorithm used here works by iterating through both strings from the start until the first different character is encountered. Then the rest of the new text is considered the difference. This doesn't produce the theoretically optimal/minimal diff, but seems to work well enough for practical application. When text is removed the diff is empty --- .gitlab-ci.yml | 4 ++-- CMakeLists.txt | 1 + autotests/documenttest.cpp | 49 ++++++++++++++++++++++++++++++++++++++ core/document.cpp | 25 +++++++++++++++++++ core/document_p.h | 2 ++ core/script/event.cpp | 11 +++++++++ core/script/event_p.h | 3 +++ core/script/kjs_event.cpp | 8 +++++++ 8 files changed, 101 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 0f26cd6a5..fa5d76a27 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -19,7 +19,7 @@ build_ubuntu_20_04: - apt-get update - apt-get install --yes eatmydata - eatmydata apt-get build-dep --yes --no-install-recommends okular - - eatmydata apt-get install --yes --no-install-recommends ninja-build + - eatmydata apt-get install --yes --no-install-recommends ninja-build qtbase5-private-dev script: - mkdir -p build && cd build - cmake -DOKULAR_UI=desktop -G Ninja .. @@ -37,7 +37,7 @@ build_clazy_clang_tidy: - apt-get update - apt-get install --yes eatmydata - eatmydata apt-get build-dep --yes --no-install-recommends okular - - eatmydata apt-get install --yes --no-install-recommends ninja-build clazy clang clang-tidy libkf5crash-dev libkf5purpose-dev kirigami2-dev libegl-dev jq + - eatmydata apt-get install --yes --no-install-recommends ninja-build clazy clang clang-tidy qtbase5-private-dev libkf5crash-dev libkf5purpose-dev kirigami2-dev libegl-dev jq script: - srcdir=`pwd` && mkdir -p /tmp/okular_build && cd /tmp/okular_build && CC=clang CXX=clazy CXXFLAGS="-Werror -Wno-deprecated-declarations" cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -G Ninja $srcdir && cat compile_commands.json | jq '[.[] | select(.file | contains("'"$srcdir"'"))]' > compile_commands.aux.json && cat compile_commands.aux.json | jq '[.[] | select(.file | contains("/synctex/")| not)]' > compile_commands.json && cp "$srcdir/.clang-tidy" . diff --git a/CMakeLists.txt b/CMakeLists.txt index 05743e8e5..98cfd2b17 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -451,6 +451,7 @@ PUBLIC # these are included from the installed headers KF5::ConfigGui Qt5::PrintSupport Qt5::Widgets + Qt5::CorePrivate ) diff --git a/autotests/documenttest.cpp b/autotests/documenttest.cpp index 0b9c3eddc..e6115e10f 100644 --- a/autotests/documenttest.cpp +++ b/autotests/documenttest.cpp @@ -29,6 +29,8 @@ class DocumentTest : public QObject private Q_SLOTS: void testCloseDuringRotationJob(); void testDocdataMigration(); + void testDiff_data(); + void testDiff(); }; // Test that we don't crash if the document is closed while a RotationJob @@ -126,5 +128,52 @@ void DocumentTest::testDocdataMigration() delete m_document; } +void DocumentTest::testDiff_data() +{ + QTest::addColumn("oldVal"); + QTest::addColumn("newVal"); + QTest::addColumn("expectedDiff"); + + QTest::addRow("empty") << "" + << "" + << ""; + QTest::addRow("a") << "" + << "a" + << "a"; + QTest::addRow("ab") << "a" + << "b" + << "b"; + QTest::addRow("ab2") << "a" + << "ab" + << "b"; + QTest::addRow("kaesekuchen") << "Käse" + << "Käsekuchen" + << "kuchen"; + QTest::addRow("replace") << "kuchen" + << "wurst" + << "wurst"; + QTest::addRow("okular") << "Oku" + << "Okular" + << "lar"; + QTest::addRow("removal1") << "a" + << "" + << ""; + QTest::addRow("removal2") << "ab" + << "a" + << ""; + QTest::addRow("unicode") << "☮🤌" + << "☮🤌❤️" + << "❤️"; +} + +void DocumentTest::testDiff() +{ + QFETCH(QString, oldVal); + QFETCH(QString, newVal); + QFETCH(QString, expectedDiff); + + QCOMPARE(Okular::DocumentPrivate::diff(oldVal, newVal), expectedDiff); +} + QTEST_MAIN(DocumentTest) #include "documenttest.moc" diff --git a/core/document.cpp b/core/document.cpp index 2d91e3a79..33ff1182b 100644 --- a/core/document.cpp +++ b/core/document.cpp @@ -47,6 +47,7 @@ #include #include #include +#include #include #include @@ -4342,6 +4343,29 @@ void Document::processFormatAction(const Action *action, Okular::FormFieldText * } } +QString DocumentPrivate::diff(const QString &oldVal, const QString &newVal) +{ + QString diff; + + QStringIterator oldIt(oldVal); + QStringIterator newIt(newVal); + + while (oldIt.hasNext() && newIt.hasNext()) { + QChar oldToken = oldIt.next(); + QChar newToken = newIt.next(); + + if (oldToken != newToken) { + diff += newToken; + break; + } + } + + while (newIt.hasNext()) { + diff += newIt.next(); + } + return diff; +} + void Document::processKeystrokeAction(const Action *action, Okular::FormFieldText *fft, const QVariant &newValue) { if (action->actionType() != Action::Script) { @@ -4357,6 +4381,7 @@ void Document::processKeystrokeAction(const Action *action, Okular::FormFieldTex } std::shared_ptr event = Event::createKeystrokeEvent(fft, d->m_pagesVector[foundPage]); + event->setChange(DocumentPrivate::diff(fft->text(), newValue.toString())); const ScriptAction *linkscript = static_cast(action); diff --git a/core/document_p.h b/core/document_p.h index f5fbe5728..070b3e334 100644 --- a/core/document_p.h +++ b/core/document_p.h @@ -230,6 +230,8 @@ public: void clearAndWaitForRequests(); + OKULARCORE_EXPORT static QString diff(const QString &oldVal, const QString &newVal); + /* * Executes a ScriptAction with the event passed as parameter. */ diff --git a/core/script/event.cpp b/core/script/event.cpp index ec0f93efb..17bba46d7 100644 --- a/core/script/event.cpp +++ b/core/script/event.cpp @@ -36,6 +36,7 @@ public: bool m_returnCode; bool m_shiftModifier; bool m_willCommit; + QString m_change; }; Event::Event() @@ -181,6 +182,16 @@ void Event::setWillCommit(bool willCommit) d->m_willCommit = willCommit; } +QString Event::change() const +{ + return d->m_change; +} + +void Event::setChange(const QString &change) +{ + d->m_change = change; +} + // static std::shared_ptr Event::createFormCalculateEvent(FormField *target, Page *targetPage, FormField *source, Page *sourcePage, const QString &targetName) { diff --git a/core/script/event_p.h b/core/script/event_p.h index f3772722d..26c107619 100644 --- a/core/script/event_p.h +++ b/core/script/event_p.h @@ -108,6 +108,9 @@ public: bool willCommit() const; void setWillCommit(bool willCommit); + QString change() const; + void setChange(const QString &change); + static std::shared_ptr createFormCalculateEvent(FormField *target, Page *targetPage, FormField *source = nullptr, Page *sourcePage = nullptr, const QString &targetName = QString()); static std::shared_ptr createFormatEvent(FormField *target, Page *targetPage, const QString &targetName = QString()); static std::shared_ptr createKeystrokeEvent(FormField *target, Page *targetPage); diff --git a/core/script/kjs_event.cpp b/core/script/kjs_event.cpp index 11b84c047..9bb44f31e 100644 --- a/core/script/kjs_event.cpp +++ b/core/script/kjs_event.cpp @@ -123,6 +123,13 @@ static KJSObject eventGetWillCommit(KJSContext *, void *object) return KJSBoolean(event->willCommit()); } +// Event.change (getter) +static KJSObject eventGetChange(KJSContext *, void *object) +{ + const Event *event = reinterpret_cast(object); + return KJSString(event->change()); +} + void JSEvent::initType(KJSContext *ctx) { static bool initialized = false; @@ -144,6 +151,7 @@ void JSEvent::initType(KJSContext *ctx) g_eventProto->defineProperty(ctx, QStringLiteral("willCommit"), eventGetWillCommit); g_eventProto->defineProperty(ctx, QStringLiteral("value"), eventGetValue, eventSetValue); g_eventProto->defineProperty(ctx, QStringLiteral("rc"), eventGetReturnCode, eventSetReturnCode); + g_eventProto->defineProperty(ctx, QStringLiteral("change"), eventGetChange); } KJSObject JSEvent::wrapEvent(KJSContext *ctx, Event *event)