diff --git a/autotests/src/katedocument_test.cpp b/autotests/src/katedocument_test.cpp index ca3f02e3..b003f324 100644 --- a/autotests/src/katedocument_test.cpp +++ b/autotests/src/katedocument_test.cpp @@ -1,740 +1,740 @@ /* This file is part of the KDE libraries Copyright (C) 2010-2018 Dominik Haumann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "katedocument_test.h" #include "moc_katedocument_test.cpp" #include #include #include #include #include #include #include #include #include #include /// TODO: is there a FindValgrind cmake command we could use to /// define this automatically? // comment this out and run the test case with: // valgrind --tool=callgrind --instr-atstart=no ./katedocument_test testSetTextPerformance // or similar // // #define USE_VALGRIND #ifdef USE_VALGRIND #include #endif using namespace KTextEditor; QTEST_MAIN(KateDocumentTest) class MovingRangeInvalidator : public QObject { Q_OBJECT public: explicit MovingRangeInvalidator(QObject *parent = nullptr) : QObject(parent) { } void addRange(MovingRange *range) { m_ranges << range; } QList ranges() const { return m_ranges; } public Q_SLOTS: void aboutToInvalidateMovingInterfaceContent() { qDeleteAll(m_ranges); m_ranges.clear(); } private: QList m_ranges; }; KateDocumentTest::KateDocumentTest() : QObject() { } KateDocumentTest::~KateDocumentTest() { } void KateDocumentTest::initTestCase() { KTextEditor::EditorPrivate::enableUnitTestMode(); } // tests: // KTextEditor::DocumentPrivate::insertText with word wrap enabled. It is checked whether the // text is correctly wrapped and whether the moving cursors maintain the correct // position. -// see also: http://bugs.kde.org/show_bug.cgi?id=168534 +// see also: https://bugs.kde.org/show_bug.cgi?id=168534 void KateDocumentTest::testWordWrap() { KTextEditor::DocumentPrivate doc(false, false); doc.setWordWrap(true); doc.setWordWrapAt(80); const QString content = QLatin1String(".........1.........2.........3.........4.........5.........6 ........7 ........8"); // space after 7 is now kept // else we kill indentation... const QString firstWrap = QLatin1String(".........1.........2.........3.........4.........5.........6 ........7 \n....x....8"); // space after 6 is now kept // else we kill indentation... const QString secondWrap = QLatin1String(".........1.........2.........3.........4.........5.........6 \n....ooooooooooo....7 ....x....8"); doc.setText(content); MovingCursor *c = doc.newMovingCursor(Cursor(0, 75), MovingCursor::MoveOnInsert); QCOMPARE(doc.text(), content); QCOMPARE(c->toCursor(), Cursor(0, 75)); // type a character at (0, 75) doc.insertText(c->toCursor(), QLatin1String("x")); QCOMPARE(doc.text(), firstWrap); QCOMPARE(c->toCursor(), Cursor(1, 5)); // set cursor to (0, 65) and type "ooooooooooo" c->setPosition(Cursor(0, 65)); doc.insertText(c->toCursor(), QLatin1String("ooooooooooo")); QCOMPARE(doc.text(), secondWrap); QCOMPARE(c->toCursor(), Cursor(1, 15)); } void KateDocumentTest::testWrapParagraph() { // Each paragraph must be kept as an own but re-wrapped nicely KTextEditor::DocumentPrivate doc(false, false); doc.setWordWrapAt(30); // Keep needed test data small const QString before = QLatin1String("aaaaa a aaaa\naaaaa aaa aa aaaa aaaa \naaaa a aaa aaaaaaa a aaaa\n\nxxxxx x\nxxxx xxxxx\nxxx xx xxxx \nxxxx xxxx x xxx xxxxxxx x xxxx"); const QString after = QLatin1String("aaaaa a aaaa aaaaa aaa aa aaaa \naaaa aaaa a aaa aaaaaaa a aaaa\n\nxxxxx x xxxx xxxxx xxx xx xxxx \nxxxx xxxx x xxx xxxxxxx x xxxx"); doc.setWordWrap(false); // First we try with disabled hard wrap doc.setText(before); doc.wrapParagraph(0, doc.lines() - 1); QCOMPARE(doc.text(), after); doc.setWordWrap(true); // Test again with enabled hard wrap, that had cause trouble due to twice wrapping doc.setText(before); doc.wrapParagraph(0, doc.lines() - 1); QCOMPARE(doc.text(), after); } void KateDocumentTest::testReplaceQStringList() { KTextEditor::DocumentPrivate doc(false, false); doc.setWordWrap(false); doc.setText( QLatin1String("asdf\n" "foo\n" "foo\n" "bar\n")); doc.replaceText(Range(1, 0, 3, 0), {"new", "text", ""}, false); QCOMPARE(doc.text(), QLatin1String("asdf\n" "new\n" "text\n" "bar\n")); } void KateDocumentTest::testMovingInterfaceSignals() { KTextEditor::DocumentPrivate *doc = new KTextEditor::DocumentPrivate; QSignalSpy aboutToDeleteSpy(doc, SIGNAL(aboutToDeleteMovingInterfaceContent(KTextEditor::Document *))); QSignalSpy aboutToInvalidateSpy(doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *))); QCOMPARE(doc->revision(), qint64(0)); QCOMPARE(aboutToInvalidateSpy.count(), 0); QCOMPARE(aboutToDeleteSpy.count(), 0); QTemporaryFile f; f.open(); doc->openUrl(QUrl::fromLocalFile(f.fileName())); QCOMPARE(doc->revision(), qint64(0)); // TODO: gets emitted once in closeFile and once in openFile - is that OK? QCOMPARE(aboutToInvalidateSpy.count(), 2); QCOMPARE(aboutToDeleteSpy.count(), 0); doc->documentReload(); QCOMPARE(doc->revision(), qint64(0)); QCOMPARE(aboutToInvalidateSpy.count(), 4); // TODO: gets emitted once in closeFile and once in openFile - is that OK? QCOMPARE(aboutToDeleteSpy.count(), 0); delete doc; QCOMPARE(aboutToInvalidateSpy.count(), 4); QCOMPARE(aboutToDeleteSpy.count(), 1); } void KateDocumentTest::testSetTextPerformance() { const int lines = 150; const int columns = 80; const int rangeLength = 4; const int rangeGap = 1; Q_ASSERT(columns % (rangeLength + rangeGap) == 0); KTextEditor::DocumentPrivate doc; MovingRangeInvalidator invalidator; connect(&doc, SIGNAL(aboutToInvalidateMovingInterfaceContent(KTextEditor::Document *)), &invalidator, SLOT(aboutToInvalidateMovingInterfaceContent())); QString text; QVector ranges; ranges.reserve(lines * columns / (rangeLength + rangeGap)); const QString line = QString().fill('a', columns); for (int l = 0; l < lines; ++l) { text.append(line); text.append('\n'); for (int c = 0; c < columns; c += rangeLength + rangeGap) { ranges << Range(l, c, l, c + rangeLength); } } // replace QBENCHMARK { // init doc.setText(text); for (const Range &range : qAsConst(ranges)) { invalidator.addRange(doc.newMovingRange(range)); } QCOMPARE(invalidator.ranges().size(), ranges.size()); #ifdef USE_VALGRIND CALLGRIND_START_INSTRUMENTATION #endif doc.setText(text); #ifdef USE_VALGRIND CALLGRIND_STOP_INSTRUMENTATION #endif QCOMPARE(doc.text(), text); QVERIFY(invalidator.ranges().isEmpty()); } } void KateDocumentTest::testRemoveTextPerformance() { const int lines = 5000; const int columns = 80; KTextEditor::DocumentPrivate doc; QString text; const QString line = QString().fill('a', columns); for (int l = 0; l < lines; ++l) { text.append(line); text.append('\n'); } doc.setText(text); // replace QBENCHMARK_ONCE { #ifdef USE_VALGRIND CALLGRIND_START_INSTRUMENTATION #endif doc.editStart(); doc.removeText(doc.documentRange()); doc.editEnd(); #ifdef USE_VALGRIND CALLGRIND_STOP_INSTRUMENTATION #endif } } void KateDocumentTest::testForgivingApiUsage() { KTextEditor::DocumentPrivate doc; QVERIFY(doc.isEmpty()); QVERIFY(doc.replaceText(Range(0, 0, 100, 100), "asdf")); QCOMPARE(doc.text(), QString("asdf")); QCOMPARE(doc.lines(), 1); QVERIFY(doc.replaceText(Range(2, 5, 2, 100), "asdf")); QCOMPARE(doc.lines(), 3); QCOMPARE(doc.text(), QLatin1String("asdf\n\n asdf")); QVERIFY(doc.removeText(Range(0, 0, 1000, 1000))); QVERIFY(doc.removeText(Range(0, 0, 0, 100))); QVERIFY(doc.isEmpty()); doc.insertText(Cursor(100, 0), "foobar"); QCOMPARE(doc.line(100), QString("foobar")); doc.setText("nY\nnYY\n"); QVERIFY(doc.removeText(Range(0, 0, 0, 1000))); } void KateDocumentTest::testAutoBrackets() { KTextEditor::DocumentPrivate doc; auto view = static_cast(doc.createView(nullptr)); auto reset = [&]() { doc.setText(""); view->setCursorPosition(Cursor(0, 0)); }; auto typeText = [&](const QString &text) { for (int i = 0; i < text.size(); ++i) { doc.typeChars(view, text.at(i)); } }; doc.setHighlightingMode("Normal"); // Just to be sure view->config()->setValue(KateViewConfig::AutoBrackets, true); QString testInput; testInput = ("("); typeText(testInput); QCOMPARE(doc.text(), "()"); reset(); testInput = ("\""); typeText(testInput); QCOMPARE(doc.text(), "\"\""); reset(); testInput = ("'"); typeText(testInput); QCOMPARE(doc.text(), "'"); // In Normal mode there is only one quote to expect // // Switch over to some other mode // doc.setHighlightingMode("C++"); reset(); typeText(testInput); QCOMPARE(doc.text(), "''"); // Now it must be two reset(); testInput = "('')"; typeText(testInput); // Known bad behaviour in case of nested brackets QCOMPARE(doc.text(), testInput); reset(); testInput = ("foo \"bar\" haz"); typeText(testInput); QCOMPARE(doc.text(), testInput); // Simulate afterwards to add quotes, bug 405089 doc.setText("foo \"bar"); typeText("\" haz"); QCOMPARE(doc.text(), testInput); // Let's check to add brackets to a selection... view->setBlockSelection(false); doc.setText("012xxx678"); view->setSelection(Range(0, 3, 0, 6)); typeText("["); QCOMPARE(doc.text(), "012[xxx]678"); QCOMPARE(view->selectionRange(), Range(0, 4, 0, 7)); // ...over multiple lines.. doc.setText("012xxx678\n012xxx678"); view->setSelection(Range(0, 3, 1, 6)); typeText("["); QCOMPARE(doc.text(), "012[xxx678\n012xxx]678"); QCOMPARE(view->selectionRange(), Range(0, 4, 1, 6)); // ..once again in in block mode with increased complexity.. view->setBlockSelection(true); doc.setText("012xxx678\n012xxx678"); view->setSelection(Range(0, 3, 1, 6)); typeText("[("); QCOMPARE(doc.text(), "012[(xxx)]678\n012[(xxx)]678"); QCOMPARE(view->selectionRange(), Range(0, 5, 1, 8)); // ..and the same with right->left selection view->setBlockSelection(true); doc.setText("012xxx678\n012xxx678"); view->setSelection(Range(0, 6, 1, 3)); typeText("[("); QCOMPARE(doc.text(), "012[(xxx)]678\n012[(xxx)]678"); QCOMPARE(view->selectionRange(), Range(0, 8, 1, 5)); } void KateDocumentTest::testReplaceTabs() { KTextEditor::DocumentPrivate doc; auto view = static_cast(doc.createView(nullptr)); auto reset = [&]() { doc.setText(" Hi!"); view->setCursorPosition(Cursor(0, 0)); }; doc.setHighlightingMode("C++"); doc.config()->setTabWidth(4); doc.config()->setIndentationMode("cppstyle"); // no replace tabs, no indent pasted text doc.setConfigValue("replace-tabs", false); doc.setConfigValue("indent-pasted-text", false); reset(); doc.insertText(Cursor(0, 0), "\t"); QCOMPARE(doc.text(), QStringLiteral("\t Hi!")); reset(); doc.typeChars(view, "\t"); QCOMPARE(doc.text(), QStringLiteral("\t Hi!")); reset(); doc.paste(view, "some\ncode\n 3\nhi\n"); QCOMPARE(doc.text(), QStringLiteral("some\ncode\n 3\nhi\n Hi!")); // now, enable replace tabs doc.setConfigValue("replace-tabs", true); reset(); doc.insertText(Cursor(0, 0), "\t"); // calling insertText does not replace tabs QCOMPARE(doc.text(), QStringLiteral("\t Hi!")); reset(); doc.typeChars(view, "\t"); // typing text replaces tabs QCOMPARE(doc.text(), QStringLiteral(" Hi!")); reset(); doc.paste(view, "\t"); // pasting text does not with indent-pasted off QCOMPARE(doc.text(), QStringLiteral("\t Hi!")); doc.setConfigValue("indent-pasted-text", true); doc.setText("int main() {\n \n}"); view->setCursorPosition(Cursor(1, 4)); doc.paste(view, "\tHi"); // ... and it still does not with indent-pasted on. // This behaviour is up to discussion. QCOMPARE(doc.text(), QStringLiteral("int main() {\n Hi\n}")); reset(); doc.paste(view, "some\ncode\n 3\nhi"); QCOMPARE(doc.text(), QStringLiteral("some\ncode\n3\nhi Hi!")); } /** * Provides slots to check data sent in specific signals. Slot names are derived from corresponding test names. */ class SignalHandler : public QObject { Q_OBJECT public Q_SLOTS: void slotMultipleLinesRemoved(KTextEditor::Document *, const KTextEditor::Range &, const QString &oldText) { QCOMPARE(oldText, QString("line2\nline3\n")); } void slotNewlineInserted(KTextEditor::Document *, const KTextEditor::Range &range) { QCOMPARE(range, Range(Cursor(1, 4), Cursor(2, 0))); } }; void KateDocumentTest::testRemoveMultipleLines() { KTextEditor::DocumentPrivate doc; doc.setText( "line1\n" "line2\n" "line3\n" "line4\n"); SignalHandler handler; connect(&doc, &KTextEditor::DocumentPrivate::textRemoved, &handler, &SignalHandler::slotMultipleLinesRemoved); doc.removeText(Range(1, 0, 3, 0)); } void KateDocumentTest::testInsertNewline() { KTextEditor::DocumentPrivate doc; doc.setText( "this is line\n" "this is line2\n"); SignalHandler handler; connect(&doc, SIGNAL(textInserted(KTextEditor::Document *, KTextEditor::Range)), &handler, SLOT(slotNewlineInserted(KTextEditor::Document *, KTextEditor::Range))); doc.editWrapLine(1, 4); } void KateDocumentTest::testInsertAfterEOF() { KTextEditor::DocumentPrivate doc; doc.setText( "line0\n" "line1"); const QString input = QLatin1String( "line3\n" "line4"); const QString expected = QLatin1String( "line0\n" "line1\n" "\n" "line3\n" "line4"); doc.insertText(KTextEditor::Cursor(3, 0), input); QCOMPARE(doc.text(), expected); } // we have two different ways of creating the checksum: // in KateFileLoader and KTextEditor::DocumentPrivate::createDigest. Make // sure, these two implementations result in the same checksum. void KateDocumentTest::testDigest() { // Git hash of test file (git hash-object data/md5checksum.txt): const QByteArray gitHash = "696e6d35a5d9cc28d16e56bdcb2d2a88126b814e"; // QCryptographicHash is used, therefore we need fromHex here const QByteArray fileDigest = QByteArray::fromHex(gitHash); // make sure, Kate::TextBuffer and KTextEditor::DocumentPrivate::createDigest() equal KTextEditor::DocumentPrivate doc; doc.openUrl(QUrl::fromLocalFile(QLatin1String(TEST_DATA_DIR "md5checksum.txt"))); const QByteArray bufferDigest(doc.checksum()); QVERIFY(doc.createDigest()); const QByteArray docDigest(doc.checksum()); QCOMPARE(bufferDigest, fileDigest); QCOMPARE(docDigest, fileDigest); } void KateDocumentTest::testModelines() { // honor document variable indent-width { KTextEditor::DocumentPrivate doc; QCOMPARE(doc.config()->indentationWidth(), 4); doc.readVariableLine(QStringLiteral("kate: indent-width 3;")); QCOMPARE(doc.config()->indentationWidth(), 3); } // honor document variable indent-width with * wildcard { KTextEditor::DocumentPrivate doc; QCOMPARE(doc.config()->indentationWidth(), 4); doc.readVariableLine(QStringLiteral("kate-wildcard(*): indent-width 3;")); QCOMPARE(doc.config()->indentationWidth(), 3); } // ignore document variable indent-width, since the wildcard does not match { KTextEditor::DocumentPrivate doc; QCOMPARE(doc.config()->indentationWidth(), 4); doc.readVariableLine(QStringLiteral("kate-wildcard(*.txt): indent-width 3;")); QCOMPARE(doc.config()->indentationWidth(), 4); } // document variable indent-width, since the wildcard does not match { KTextEditor::DocumentPrivate doc; doc.openUrl(QUrl::fromLocalFile(QLatin1String(TEST_DATA_DIR "modelines.txt"))); QVERIFY(!doc.isEmpty()); // ignore wrong wildcard QCOMPARE(doc.config()->indentationWidth(), 4); doc.readVariableLine(QStringLiteral("kate-wildcard(*.bar): indent-width 3;")); QCOMPARE(doc.config()->indentationWidth(), 4); // read correct wildcard QCOMPARE(doc.config()->indentationWidth(), 4); doc.readVariableLine(QStringLiteral("kate-wildcard(*.txt): indent-width 5;")); QCOMPARE(doc.config()->indentationWidth(), 5); // honor correct wildcard QCOMPARE(doc.config()->indentationWidth(), 5); doc.readVariableLine(QStringLiteral("kate-wildcard(*.foo;*.txt;*.bar): indent-width 6;")); QCOMPARE(doc.config()->indentationWidth(), 6); // ignore incorrect mimetype QCOMPARE(doc.config()->indentationWidth(), 6); doc.readVariableLine(QStringLiteral("kate-mimetype(text/unknown): indent-width 7;")); QCOMPARE(doc.config()->indentationWidth(), 6); // honor correct mimetype QCOMPARE(doc.config()->indentationWidth(), 6); doc.readVariableLine(QStringLiteral("kate-mimetype(text/plain): indent-width 8;")); QCOMPARE(doc.config()->indentationWidth(), 8); // honor correct mimetype QCOMPARE(doc.config()->indentationWidth(), 8); doc.readVariableLine(QStringLiteral("kate-mimetype(text/foo;text/plain;text/bar): indent-width 9;")); QCOMPARE(doc.config()->indentationWidth(), 9); } } void KateDocumentTest::testDefStyleNum() { KTextEditor::DocumentPrivate doc; doc.setText("foo\nbar\nasdf"); QCOMPARE(doc.defStyleNum(0, 0), 0); } void KateDocumentTest::testTypeCharsWithSurrogateAndNewLine() { KTextEditor::DocumentPrivate doc; auto view = static_cast(doc.createView(nullptr)); const uint surrogateUcs4String[] = {0x1f346, '\n', 0x1f346, 0}; const auto surrogateString = QString::fromUcs4(surrogateUcs4String); doc.typeChars(view, surrogateString); QCOMPARE(doc.text(), surrogateString); } void KateDocumentTest::testRemoveComposedCharacters() { KTextEditor::DocumentPrivate doc; auto view = static_cast(doc.createView(nullptr)); view->config()->setValue(KateViewConfig::BackspaceRemoveComposedCharacters, true); doc.setText(QString::fromUtf8("व्यक्तियों")); doc.del(view, Cursor(0, 0)); QCOMPARE(doc.text(), QString::fromUtf8(("क्तियों"))); doc.backspace(view, Cursor(0, 7)); QCOMPARE(doc.text(), QString::fromUtf8(("क्ति"))); } void KateDocumentTest::testAutoReload() { QTemporaryFile file("AutoReloadTestFile"); file.open(); QTextStream stream(&file); stream << "Hello"; stream.flush(); KTextEditor::DocumentPrivate doc; auto view = static_cast(doc.createView(nullptr)); QVERIFY(doc.openUrl(QUrl::fromLocalFile(file.fileName()))); QCOMPARE(doc.text(), "Hello"); // The cursor should be and stay in the last line... QCOMPARE(view->cursorPosition().line(), doc.documentEnd().line()); doc.autoReloadToggled(true); // Some magic value. You can wait as long as you like after write to file, // without to wait before it doesn't work here. It mostly fails with 500, // it tend to work with 600, but is not guarantied to work even with 800 QTest::qWait(1000); stream << "\nTest"; stream.flush(); // Hardcoded delay m_modOnHdTimer in DocumentPrivate // + min val with which it looks working reliable here QTest::qWait(200 + 800); QCOMPARE(doc.text(), "Hello\nTest"); // ...stay in the last line after reload! QCOMPARE(view->cursorPosition().line(), doc.documentEnd().line()); // Now we have more then one line, set the cursor to some line != last line... Cursor c(0, 3); view->setCursorPosition(c); stream << "\nWorld!"; stream.flush(); file.close(); QTest::qWait(200 + 800); QCOMPARE(doc.text(), "Hello\nTest\nWorld!"); // ...and ensure we have not move around QCOMPARE(view->cursorPosition(), c); } void KateDocumentTest::testSearch() { /** * this is the start of some new implementation of searchText that can handle multi-line regex stuff * just naturally and without always concatenating the full document. */ KTextEditor::DocumentPrivate doc; doc.setText("foo\nbar\nzonk"); const QRegularExpression pattern(QStringLiteral("ar\nzonk$"), QRegularExpression::MultilineOption); int startLine = 0; int endLine = 2; QString textToMatch; int partialMatchLine = -1; for (int currentLine = startLine; currentLine <= endLine; ++currentLine) { // if we had a partial match before, we need to keep the old text and append our new line int matchStartLine = currentLine; if (partialMatchLine >= 0) { textToMatch += doc.line(currentLine); textToMatch += QLatin1Char('\n'); matchStartLine = partialMatchLine; } else { textToMatch = doc.line(currentLine); textToMatch += QLatin1Char('\n'); } auto result = pattern.match(textToMatch, 0, QRegularExpression::PartialPreferFirstMatch); printf("lala %d %d %d %d\n", result.hasMatch(), result.hasPartialMatch(), result.capturedStart(), result.capturedLength()); if (result.hasMatch()) { printf("found stuff starting in line %d\n", matchStartLine); break; } // else: remember if we need to keep text! if (result.hasPartialMatch()) { // if we had already a partial match before, keep the line! if (partialMatchLine >= 0) { } else { partialMatchLine = currentLine; } } else { // we can forget the old text partialMatchLine = -1; } } } #include "katedocument_test.moc" diff --git a/autotests/src/katefoldingtest.cpp b/autotests/src/katefoldingtest.cpp index ed91192a..0bec6cf1 100644 --- a/autotests/src/katefoldingtest.cpp +++ b/autotests/src/katefoldingtest.cpp @@ -1,152 +1,152 @@ /* This file is part of the Kate project. * * Copyright (C) 2013 Dominik Haumann * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 of the License, or (at your option) any later version. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "katefoldingtest.h" #include #include #include #include #include #include #include using namespace KTextEditor; QTEST_MAIN(KateFoldingTest) void KateFoldingTest::initTestCase() { KTextEditor::EditorPrivate::enableUnitTestMode(); } void KateFoldingTest::cleanupTestCase() { } -// This is a unit test for bug 311866 (http://bugs.kde.org/show_bug.cgi?id=311866) +// This is a unit test for bug 311866 (https://bugs.kde.org/show_bug.cgi?id=311866) // It loads 5 lines of C++ code, places the cursor in line 4, and then folds // the code. // Expected behavior: the cursor should be moved so it stays visible // Buggy behavior: the cursor is hidden, and moving the hidden cursor crashes kate void KateFoldingTest::testCrash311866() { KTextEditor::DocumentPrivate doc; const QUrl url = QUrl::fromLocalFile(QLatin1String(TEST_DATA_DIR "bug311866.cpp")); doc.openUrl(url); doc.setHighlightingMode("C++"); doc.buffer().ensureHighlighted(6); KTextEditor::ViewPrivate *view = static_cast(doc.createView(nullptr)); view->show(); view->resize(400, 300); view->setCursorPosition(Cursor(3, 0)); QTest::qWait(100); view->slotFoldToplevelNodes(); doc.buffer().ensureHighlighted(6); qDebug() << "!!! Does the next line crash?"; view->up(); } // This test makes sure that, // - if you have selected text // - that spans a folded range, // - and the cursor is at the end of the text selection, // - and you type a char, e.g. 'x', // then the resulting text is correct, and changing region // visibility does not mess around with the text cursor. // // See https://bugs.kde.org/show_bug.cgi?id=295632 void KateFoldingTest::testBug295632() { KTextEditor::DocumentPrivate doc; QString text = "oooossssssss\n" "{\n" "\n" "}\n" "ssssss----------"; doc.setText(text); // view must be visible... KTextEditor::ViewPrivate *view = static_cast(doc.createView(nullptr)); view->show(); view->resize(400, 300); qint64 foldId = view->textFolding().newFoldingRange(KTextEditor::Range(1, 0, 3, 1)); view->textFolding().foldRange(foldId); QVERIFY(view->textFolding().isLineVisible(0)); QVERIFY(view->textFolding().isLineVisible(1)); QVERIFY(!view->textFolding().isLineVisible(2)); QVERIFY(!view->textFolding().isLineVisible(3)); QVERIFY(view->textFolding().isLineVisible(4)); view->setSelection(Range(Cursor(0, 4), Cursor(4, 6))); view->setCursorPosition(Cursor(4, 6)); QTest::qWait(100); doc.typeChars(view, "x"); QTest::qWait(100); QString line = doc.line(0); QCOMPARE(line, QString("oooox----------")); } // This testcase tests the following: // - the cursor is first set into the word 'hello' // - then lines 0-3 are folded. // - the real text cursor is still in the word 'hello' // - the important issue is: The display cursor must be in the visible line range // --> if this test passes, KateViewInternal's m_displayCursor is properly adapted. void KateFoldingTest::testCrash367466() { KTextEditor::DocumentPrivate doc; QString text = "fold begin\n" "\n" "\n" "fold end\n" "hello\n" "world\n"; doc.setText(text); // view must be visible... KTextEditor::ViewPrivate *view = static_cast(doc.createView(nullptr)); view->show(); view->resize(400, 300); view->setCursorPosition(KTextEditor::Cursor(5, 2)); QCOMPARE(view->cursorPosition(), KTextEditor::Cursor(5, 2)); qint64 foldId = view->textFolding().newFoldingRange(KTextEditor::Range(0, 0, 3, 8)); view->textFolding().foldRange(foldId); QVERIFY(view->textFolding().isLineVisible(0)); QVERIFY(!view->textFolding().isLineVisible(1)); QVERIFY(!view->textFolding().isLineVisible(2)); QVERIFY(!view->textFolding().isLineVisible(3)); QVERIFY(view->textFolding().isLineVisible(4)); QVERIFY(view->textFolding().isLineVisible(5)); QCOMPARE(view->cursorPosition(), KTextEditor::Cursor(5, 2)); view->up(); QCOMPARE(view->cursorPosition(), KTextEditor::Cursor(4, 2)); } diff --git a/autotests/src/kte_documentcursor.cpp b/autotests/src/kte_documentcursor.cpp index 8b091961..3c782cad 100644 --- a/autotests/src/kte_documentcursor.cpp +++ b/autotests/src/kte_documentcursor.cpp @@ -1,361 +1,361 @@ /* This file is part of the KDE libraries Copyright (C) 2012-2018 Dominik Haumann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kte_documentcursor.h" #include #include #include #include #include QTEST_MAIN(DocumentCursorTest) using namespace KTextEditor; DocumentCursorTest::DocumentCursorTest() : QObject() { } DocumentCursorTest::~DocumentCursorTest() { } void DocumentCursorTest::initTestCase() { KTextEditor::EditorPrivate::enableUnitTestMode(); } void DocumentCursorTest::cleanupTestCase() { } // tests: // - atStartOfDocument // - atStartOfLine // - atEndOfDocument // - atEndOfLine // - move forward with Wrap // - move forward with NoWrap // - move backward // - gotoNextLine // - gotoPreviousLine void DocumentCursorTest::testConvenienceApi() { KTextEditor::DocumentPrivate doc; doc.setText( "\n" "1\n" "22\n" "333\n" "4444\n" "55555"); // check start and end of document DocumentCursor startOfDoc(&doc); startOfDoc.setPosition(Cursor(0, 0)); DocumentCursor endOfDoc(&doc); endOfDoc.setPosition(Cursor(5, 5)); QVERIFY(startOfDoc.atStartOfDocument()); QVERIFY(startOfDoc.atStartOfLine()); QVERIFY(endOfDoc.atEndOfDocument()); QVERIFY(endOfDoc.atEndOfLine()); // set cursor to (2, 2) and then move to the left two times DocumentCursor moving(&doc); moving.setPosition(Cursor(2, 2)); QVERIFY(moving.atEndOfLine()); // at 2, 2 QVERIFY(moving.move(-1)); // at 2, 1 QCOMPARE(moving.toCursor(), Cursor(2, 1)); QVERIFY(!moving.atEndOfLine()); QVERIFY(moving.move(-1)); // at 2, 0 QCOMPARE(moving.toCursor(), Cursor(2, 0)); QVERIFY(moving.atStartOfLine()); // now move again to the left, should wrap to (1, 1) QVERIFY(moving.move(-1)); // at 1, 1 QCOMPARE(moving.toCursor(), Cursor(1, 1)); QVERIFY(moving.atEndOfLine()); // advance 7 characters to position (3, 3) QVERIFY(moving.move(7)); // at 3, 3 QCOMPARE(moving.toCursor(), Cursor(3, 3)); // advance 20 characters in NoWrap mode, then go back 10 characters QVERIFY(moving.move(20, DocumentCursor::NoWrap)); // at 3, 23 QCOMPARE(moving.toCursor(), Cursor(3, 23)); QVERIFY(moving.move(-10)); // at 3, 13 QCOMPARE(moving.toCursor(), Cursor(3, 13)); // still at invalid text position. move one char to wrap around QVERIFY(!moving.isValidTextPosition()); // at 3, 13 QVERIFY(moving.move(1)); // at 4, 0 QCOMPARE(moving.toCursor(), Cursor(4, 0)); // moving 11 characters in wrap mode moves to (5, 6), which is not a valid // text position anymore. Hence, moving should be rejected. QVERIFY(!moving.move(11)); QVERIFY(moving.move(10)); QVERIFY(moving.atEndOfDocument()); // try to move to next line, which fails. then go to previous line QVERIFY(!moving.gotoNextLine()); QVERIFY(moving.gotoPreviousLine()); QCOMPARE(moving.toCursor(), Cursor(4, 0)); } void DocumentCursorTest::testOperators() { KTextEditor::DocumentPrivate doc; doc.setText( "--oo--\n" "--oo--\n" "--oo--"); // create lots of cursors for comparison Cursor invalid = Cursor::invalid(); Cursor c02(0, 2); Cursor c04(0, 4); Cursor c14(1, 4); DocumentCursor m02(&doc); DocumentCursor m04(&doc); DocumentCursor m14(&doc); QVERIFY(m02 == invalid); QVERIFY(m04 == invalid); QVERIFY(m14 == invalid); m02.setPosition(Cursor(0, 2)); m04.setPosition(Cursor(0, 4)); m14.setPosition(Cursor(1, 4)); // invalid comparison // cppcheck-suppress duplicateExpression QVERIFY(invalid == invalid); QVERIFY(invalid <= c02); QVERIFY(invalid < c02); QVERIFY(!(invalid > c02)); QVERIFY(!(invalid >= c02)); QVERIFY(!(invalid == m02)); QVERIFY(invalid <= m02); QVERIFY(invalid < m02); QVERIFY(!(invalid > m02)); QVERIFY(!(invalid >= m02)); QVERIFY(!(m02 == invalid)); QVERIFY(!(m02 <= invalid)); QVERIFY(!(m02 < invalid)); QVERIFY(m02 > invalid); QVERIFY(m02 >= invalid); // DocumentCursor <-> DocumentCursor // cppcheck-suppress duplicateExpression QVERIFY(m02 == m02); // cppcheck-suppress duplicateExpression QVERIFY(m02 <= m02); // cppcheck-suppress duplicateExpression QVERIFY(m02 >= m02); // cppcheck-suppress duplicateExpression QVERIFY(!(m02 < m02)); // cppcheck-suppress duplicateExpression QVERIFY(!(m02 > m02)); // cppcheck-suppress duplicateExpression QVERIFY(!(m02 != m02)); QVERIFY(!(m02 == m04)); QVERIFY(m02 <= m04); QVERIFY(!(m02 >= m04)); QVERIFY(m02 < m04); QVERIFY(!(m02 > m04)); QVERIFY(m02 != m04); QVERIFY(!(m04 == m02)); QVERIFY(!(m04 <= m02)); QVERIFY(m04 >= m02); QVERIFY(!(m04 < m02)); QVERIFY(m04 > m02); QVERIFY(m04 != m02); QVERIFY(!(m02 == m14)); QVERIFY(m02 <= m14); QVERIFY(!(m02 >= m14)); QVERIFY(m02 < m14); QVERIFY(!(m02 > m14)); QVERIFY(m02 != m14); QVERIFY(!(m14 == m02)); QVERIFY(!(m14 <= m02)); QVERIFY(m14 >= m02); QVERIFY(!(m14 < m02)); QVERIFY(m14 > m02); QVERIFY(m14 != m02); // DocumentCursor <-> Cursor QVERIFY(m02 == c02); QVERIFY(m02 <= c02); QVERIFY(m02 >= c02); QVERIFY(!(m02 < c02)); QVERIFY(!(m02 > c02)); QVERIFY(!(m02 != c02)); QVERIFY(!(m02 == c04)); QVERIFY(m02 <= c04); QVERIFY(!(m02 >= c04)); QVERIFY(m02 < c04); QVERIFY(!(m02 > c04)); QVERIFY(m02 != c04); QVERIFY(!(m04 == c02)); QVERIFY(!(m04 <= c02)); QVERIFY(m04 >= c02); QVERIFY(!(m04 < c02)); QVERIFY(m04 > c02); QVERIFY(m04 != c02); QVERIFY(!(m02 == c14)); QVERIFY(m02 <= c14); QVERIFY(!(m02 >= c14)); QVERIFY(m02 < c14); QVERIFY(!(m02 > c14)); QVERIFY(m02 != c14); QVERIFY(!(m14 == c02)); QVERIFY(!(m14 <= c02)); QVERIFY(m14 >= c02); QVERIFY(!(m14 < c02)); QVERIFY(m14 > c02); QVERIFY(m14 != c02); // Cursor <-> DocumentCursor QVERIFY(c02 == m02); QVERIFY(c02 <= m02); QVERIFY(c02 >= m02); QVERIFY(!(c02 < m02)); QVERIFY(!(c02 > m02)); QVERIFY(!(c02 != m02)); QVERIFY(!(c02 == m04)); QVERIFY(c02 <= m04); QVERIFY(!(c02 >= m04)); QVERIFY(c02 < m04); QVERIFY(!(c02 > m04)); QVERIFY(c02 != m04); QVERIFY(!(c04 == m02)); QVERIFY(!(c04 <= m02)); QVERIFY(c04 >= m02); QVERIFY(!(c04 < m02)); QVERIFY(c04 > m02); QVERIFY(c04 != m02); QVERIFY(!(c02 == m14)); QVERIFY(c02 <= m14); QVERIFY(!(c02 >= m14)); QVERIFY(c02 < m14); QVERIFY(!(c02 > m14)); QVERIFY(c02 != m14); QVERIFY(!(c14 == m02)); QVERIFY(!(c14 <= m02)); QVERIFY(c14 >= m02); QVERIFY(!(c14 < m02)); QVERIFY(c14 > m02); QVERIFY(c14 != m02); } void DocumentCursorTest::testValidTextPosition() { // test: utf-32 characters that are visually only one single character (Unicode surrogates) // Inside such a utf-32 character, the text position is not valid. KTextEditor::DocumentPrivate doc; DocumentCursor c(&doc); // 0xfffe: byte-order-mark (BOM) // 0x002d: '-' - // 0x3dd8, 0x38de: an utf32-surrogate, see http://www.fileformat.info/info/unicode/char/1f638/browsertest.htm + // 0x3dd8, 0x38de: an utf32-surrogate, see https://www.fileformat.info/info/unicode/char/1f638/browsertest.htm const unsigned short line0[] = {0xfffe, 0x2d00, 0x3dd8, 0x38de, 0x2d00}; // -xx- where xx is one utf32 char const unsigned short line1[] = {0xfffe, 0x2d00, 0x3dd8, 0x2d00, 0x2d00}; // -x-- where x is a high surrogate (broken utf32) const unsigned short line2[] = {0xfffe, 0x2d00, 0x2d00, 0x38de, 0x2d00}; // --x- where x is a low surrogate (broken utf32) doc.setText(QString::fromUtf16(line0, 5)); doc.insertLine(1, QString::fromUtf16(line1, 5)); doc.insertLine(2, QString::fromUtf16(line2, 5)); // set to true if you want to see the contents const bool showView = false; if (showView) { doc.createView(nullptr)->show(); QTest::qWait(5000); } // line 0: invalid in utf-32 char c.setPosition(0, 0); QVERIFY(c.isValidTextPosition()); c.setPosition(0, 1); QVERIFY(c.isValidTextPosition()); c.setPosition(0, 2); QVERIFY(!c.isValidTextPosition()); c.setPosition(0, 3); QVERIFY(c.isValidTextPosition()); c.setPosition(0, 4); QVERIFY(c.isValidTextPosition()); c.setPosition(0, 5); QVERIFY(!c.isValidTextPosition()); // line 1, valid in broken utf-32 char (2nd part missing) c.setPosition(1, 0); QVERIFY(c.isValidTextPosition()); c.setPosition(1, 1); QVERIFY(c.isValidTextPosition()); c.setPosition(1, 2); QVERIFY(c.isValidTextPosition()); c.setPosition(1, 3); QVERIFY(c.isValidTextPosition()); c.setPosition(1, 4); QVERIFY(c.isValidTextPosition()); c.setPosition(1, 5); QVERIFY(!c.isValidTextPosition()); // line 2, valid in broken utf-32 char (1st part missing) c.setPosition(2, 0); QVERIFY(c.isValidTextPosition()); c.setPosition(2, 1); QVERIFY(c.isValidTextPosition()); c.setPosition(2, 2); QVERIFY(c.isValidTextPosition()); c.setPosition(2, 3); QVERIFY(c.isValidTextPosition()); c.setPosition(2, 4); QVERIFY(c.isValidTextPosition()); c.setPosition(2, 5); QVERIFY(!c.isValidTextPosition()); // test out-of-range c.setPosition(-1, 0); QVERIFY(!c.isValidTextPosition()); c.setPosition(3, 0); QVERIFY(!c.isValidTextPosition()); c.setPosition(0, -1); QVERIFY(!c.isValidTextPosition()); } #include "moc_kte_documentcursor.cpp" diff --git a/cmake/FindEditorConfig.cmake b/cmake/FindEditorConfig.cmake index 9d5905e2..d101b022 100644 --- a/cmake/FindEditorConfig.cmake +++ b/cmake/FindEditorConfig.cmake @@ -1,63 +1,63 @@ #.rest: # FindEditorConfig # -------------- # # Try to find EditorConfig on this system. # # This will define the following variables: # # ``EditorConfig_FOUND`` # True if inotify is available # ``EditorConfig_LIBRARIES`` # This has to be passed to target_link_libraries() # ``EditorConfig_INCLUDE_DIRS`` # This has to be passed to target_include_directories() #============================================================================= # Copyright 2017 Christoph Cullmann # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # 1. Redistributions of source code must retain the copyright # notice, this list of conditions and the following disclaimer. # 2. Redistributions in binary form must reproduce the copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #============================================================================= find_path(EditorConfig_INCLUDE_DIRS editorconfig/editorconfig.h) if(EditorConfig_INCLUDE_DIRS) find_library(EditorConfig_LIBRARIES NAMES editorconfig) include(FindPackageHandleStandardArgs) find_package_handle_standard_args(EditorConfig FOUND_VAR EditorConfig_FOUND REQUIRED_VARS EditorConfig_LIBRARIES EditorConfig_INCLUDE_DIRS ) mark_as_advanced(EditorConfig_LIBRARIES EditorConfig_INCLUDE_DIRS) include(FeatureSummary) set_package_properties(EditorConfig PROPERTIES - URL "http://editorconfig.org/" + URL "https://editorconfig.org/" DESCRIPTION "EditorConfig editor configuration file support." ) else() set(EditorConfig_FOUND FALSE) endif() mark_as_advanced(EditorConfig_LIBRARIES EditorConfig_INCLUDE_DIRS) diff --git a/src/render/katerenderer.cpp b/src/render/katerenderer.cpp index 88008591..142259a1 100644 --- a/src/render/katerenderer.cpp +++ b/src/render/katerenderer.cpp @@ -1,1205 +1,1205 @@ /* This file is part of the KDE libraries Copyright (C) 2007 Mirko Stocker Copyright (C) 2003-2005 Hamish Rodda Copyright (C) 2001 Christoph Cullmann Copyright (C) 2001 Joseph Wenninger Copyright (C) 1999 Jochen Wilhelmy Copyright (C) 2013 Andrey Matveyakin 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.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "katerenderer.h" #include "inlinenotedata.h" #include "katebuffer.h" #include "kateconfig.h" #include "katedocument.h" #include "katehighlight.h" #include "katerenderrange.h" #include "katetextlayout.h" #include "kateview.h" #include "ktexteditor/inlinenote.h" #include "ktexteditor/inlinenoteprovider.h" #include "katepartdebug.h" #include #include #include #include #include #include #include // qCeil static const QChar tabChar(QLatin1Char('\t')); static const QChar spaceChar(QLatin1Char(' ')); static const QChar nbSpaceChar(0xa0); // non-breaking space KateRenderer::KateRenderer(KTextEditor::DocumentPrivate *doc, Kate::TextFolding &folding, KTextEditor::ViewPrivate *view) : m_doc(doc) , m_folding(folding) , m_view(view) , m_tabWidth(m_doc->config()->tabWidth()) , m_indentWidth(m_doc->config()->indentationWidth()) , m_caretStyle(KateRenderer::Line) , m_drawCaret(true) , m_showSelections(true) , m_showTabs(true) , m_showSpaces(KateDocumentConfig::Trailing) , m_showNonPrintableSpaces(false) , m_printerFriendly(false) , m_config(new KateRendererConfig(this)) , m_font(m_config->baseFont()) , m_fontMetrics(m_font) { updateAttributes(); // initialize with a sane font height updateFontHeight(); // make the proper calculation for markerSize updateMarkerSize(); } KateRenderer::~KateRenderer() { delete m_config; } void KateRenderer::updateAttributes() { m_attributes = m_doc->highlight()->attributes(config()->schema()); } KTextEditor::Attribute::Ptr KateRenderer::attribute(uint pos) const { if (pos < (uint)m_attributes.count()) { return m_attributes[pos]; } return m_attributes[0]; } KTextEditor::Attribute::Ptr KateRenderer::specificAttribute(int context) const { if (context >= 0 && context < m_attributes.count()) { return m_attributes[context]; } return m_attributes[0]; } void KateRenderer::setDrawCaret(bool drawCaret) { m_drawCaret = drawCaret; } void KateRenderer::setCaretStyle(KateRenderer::caretStyles style) { m_caretStyle = style; } void KateRenderer::setShowTabs(bool showTabs) { m_showTabs = showTabs; } void KateRenderer::setShowSpaces(KateDocumentConfig::WhitespaceRendering showSpaces) { m_showSpaces = showSpaces; } void KateRenderer::setShowNonPrintableSpaces(const bool on) { m_showNonPrintableSpaces = on; } void KateRenderer::setTabWidth(int tabWidth) { m_tabWidth = tabWidth; } bool KateRenderer::showIndentLines() const { return m_config->showIndentationLines(); } void KateRenderer::setShowIndentLines(bool showIndentLines) { m_config->setShowIndentationLines(showIndentLines); } void KateRenderer::setIndentWidth(int indentWidth) { m_indentWidth = indentWidth; } void KateRenderer::setShowSelections(bool showSelections) { m_showSelections = showSelections; } void KateRenderer::increaseFontSizes(qreal step) { QFont f(config()->baseFont()); f.setPointSizeF(f.pointSizeF() + step); config()->setFont(f); } void KateRenderer::resetFontSizes() { QFont f(KateRendererConfig::global()->baseFont()); config()->setFont(f); } void KateRenderer::decreaseFontSizes(qreal step) { QFont f(config()->baseFont()); if ((f.pointSizeF() - step) > 0) { f.setPointSizeF(f.pointSizeF() - step); } config()->setFont(f); } bool KateRenderer::isPrinterFriendly() const { return m_printerFriendly; } void KateRenderer::setPrinterFriendly(bool printerFriendly) { m_printerFriendly = printerFriendly; setShowTabs(false); setShowSpaces(KateDocumentConfig::None); setShowSelections(false); setDrawCaret(false); } void KateRenderer::paintTextLineBackground(QPainter &paint, KateLineLayoutPtr layout, int currentViewLine, int xStart, int xEnd) { if (isPrinterFriendly()) { return; } // Normal background color QColor backgroundColor(config()->backgroundColor()); // paint the current line background if we're on the current line QColor currentLineColor = config()->highlightedLineColor(); // Check for mark background int markRed = 0, markGreen = 0, markBlue = 0, markCount = 0; // Retrieve marks for this line uint mrk = m_doc->mark(layout->line()); if (mrk) { for (uint bit = 0; bit < 32; bit++) { KTextEditor::MarkInterface::MarkTypes markType = (KTextEditor::MarkInterface::MarkTypes)(1 << bit); if (mrk & markType) { QColor markColor = config()->lineMarkerColor(markType); if (markColor.isValid()) { markCount++; markRed += markColor.red(); markGreen += markColor.green(); markBlue += markColor.blue(); } } } // for } // Marks if (markCount) { markRed /= markCount; markGreen /= markCount; markBlue /= markCount; backgroundColor.setRgb(int((backgroundColor.red() * 0.9) + (markRed * 0.1)), int((backgroundColor.green() * 0.9) + (markGreen * 0.1)), int((backgroundColor.blue() * 0.9) + (markBlue * 0.1))); } // Draw line background paint.fillRect(0, 0, xEnd - xStart, lineHeight() * layout->viewLineCount(), backgroundColor); // paint the current line background if we're on the current line if (currentViewLine != -1) { if (markCount) { markRed /= markCount; markGreen /= markCount; markBlue /= markCount; currentLineColor.setRgb(int((currentLineColor.red() * 0.9) + (markRed * 0.1)), int((currentLineColor.green() * 0.9) + (markGreen * 0.1)), int((currentLineColor.blue() * 0.9) + (markBlue * 0.1))); } paint.fillRect(0, lineHeight() * currentViewLine, xEnd - xStart, lineHeight(), currentLineColor); } } void KateRenderer::paintTabstop(QPainter &paint, qreal x, qreal y) { QPen penBackup(paint.pen()); QPen pen(config()->tabMarkerColor()); pen.setWidthF(qMax(1.0, spaceWidth() / 10.0)); paint.setPen(pen); int dist = spaceWidth() * 0.3; QPoint points[8]; points[0] = QPoint(x - dist, y - dist); points[1] = QPoint(x, y); points[2] = QPoint(x, y); points[3] = QPoint(x - dist, y + dist); x += spaceWidth() / 3.0; points[4] = QPoint(x - dist, y - dist); points[5] = QPoint(x, y); points[6] = QPoint(x, y); points[7] = QPoint(x - dist, y + dist); paint.drawLines(points, 4); paint.setPen(penBackup); } void KateRenderer::paintSpace(QPainter &paint, qreal x, qreal y) { QPen penBackup(paint.pen()); QPen pen(config()->tabMarkerColor()); pen.setWidthF(m_markerSize); pen.setCapStyle(Qt::RoundCap); paint.setPen(pen); paint.setRenderHint(QPainter::Antialiasing, true); paint.drawPoint(QPointF(x, y)); paint.setPen(penBackup); paint.setRenderHint(QPainter::Antialiasing, false); } void KateRenderer::paintNonBreakSpace(QPainter &paint, qreal x, qreal y) { QPen penBackup(paint.pen()); QPen pen(config()->tabMarkerColor()); pen.setWidthF(qMax(1.0, spaceWidth() / 10.0)); paint.setPen(pen); const int height = fontHeight(); const int width = spaceWidth(); QPoint points[6]; points[0] = QPoint(x + width / 10, y + height / 4); points[1] = QPoint(x + width / 10, y + height / 3); points[2] = QPoint(x + width / 10, y + height / 3); points[3] = QPoint(x + width - width / 10, y + height / 3); points[4] = QPoint(x + width - width / 10, y + height / 3); points[5] = QPoint(x + width - width / 10, y + height / 4); paint.drawLines(points, 3); paint.setPen(penBackup); } void KateRenderer::paintNonPrintableSpaces(QPainter &paint, qreal x, qreal y, const QChar &chr) { paint.save(); QPen pen(config()->spellingMistakeLineColor()); pen.setWidthF(qMax(1.0, spaceWidth() * 0.1)); paint.setPen(pen); const int height = fontHeight(); const int width = m_fontMetrics.boundingRect(chr).width(); const int offset = spaceWidth() * 0.1; QPoint points[8]; points[0] = QPoint(x - offset, y + offset); points[1] = QPoint(x + width + offset, y + offset); points[2] = QPoint(x + width + offset, y + offset); points[3] = QPoint(x + width + offset, y - height - offset); points[4] = QPoint(x + width + offset, y - height - offset); points[5] = QPoint(x - offset, y - height - offset); points[6] = QPoint(x - offset, y - height - offset); points[7] = QPoint(x - offset, y + offset); paint.drawLines(points, 4); paint.restore(); } void KateRenderer::paintIndentMarker(QPainter &paint, uint x, uint y /*row*/) { QPen penBackup(paint.pen()); QPen myPen(config()->indentationLineColor()); static const QVector dashPattern = QVector() << 1 << 1; myPen.setDashPattern(dashPattern); if (y % 2) { myPen.setDashOffset(1); } paint.setPen(myPen); const int height = fontHeight(); const int top = 0; const int bottom = height - 1; QPainter::RenderHints renderHints = paint.renderHints(); paint.setRenderHints(renderHints, false); paint.drawLine(x + 2, top, x + 2, bottom); paint.setRenderHints(renderHints, true); paint.setPen(penBackup); } static bool rangeLessThanForRenderer(const Kate::TextRange *a, const Kate::TextRange *b) { // compare Z-Depth first // smaller Z-Depths should win! if (a->zDepth() > b->zDepth()) { return true; } else if (a->zDepth() < b->zDepth()) { return false; } // end of a > end of b? if (a->end().toCursor() > b->end().toCursor()) { return true; } // if ends are equal, start of a < start of b? if (a->end().toCursor() == b->end().toCursor()) { return a->start().toCursor() < b->start().toCursor(); } return false; } QVector KateRenderer::decorationsForLine(const Kate::TextLine &textLine, int line, bool selectionsOnly, bool completionHighlight, bool completionSelected) const { // limit number of attributes we can highlight in reasonable time const int limitOfRanges = 1024; auto rangesWithAttributes = m_doc->buffer().rangesForLine(line, m_printerFriendly ? nullptr : m_view, true); if (rangesWithAttributes.size() > limitOfRanges) { rangesWithAttributes.clear(); } // Don't compute the highlighting if there isn't going to be any highlighting const auto &al = textLine->attributesList(); if (!(selectionsOnly || !al.isEmpty() || !rangesWithAttributes.isEmpty())) { return QVector(); } // Add the inbuilt highlighting to the list, limit with limitOfRanges RenderRangeVector renderRanges; if (!al.isEmpty()) { auto ¤tRange = renderRanges.pushNewRange(); for (int i = 0; i < std::min(al.count(), limitOfRanges); ++i) { if (al[i].length > 0 && al[i].attributeValue > 0) { currentRange.addRange(KTextEditor::Range(KTextEditor::Cursor(line, al[i].offset), al[i].length), specificAttribute(al[i].attributeValue)); } } } if (!completionHighlight) { // check for dynamic hl stuff const QSet *rangesMouseIn = m_view ? m_view->rangesMouseIn() : nullptr; const QSet *rangesCaretIn = m_view ? m_view->rangesCaretIn() : nullptr; bool anyDynamicHlsActive = m_view && (!rangesMouseIn->empty() || !rangesCaretIn->empty()); // sort all ranges, we want that the most specific ranges win during rendering, multiple equal ranges are kind of random, still better than old smart rangs behavior ;) std::sort(rangesWithAttributes.begin(), rangesWithAttributes.end(), rangeLessThanForRenderer); // loop over all ranges for (int i = 0; i < rangesWithAttributes.size(); ++i) { // real range Kate::TextRange *kateRange = rangesWithAttributes[i]; // calculate attribute, default: normal attribute KTextEditor::Attribute::Ptr attribute = kateRange->attribute(); if (anyDynamicHlsActive) { // check mouse in if (KTextEditor::Attribute::Ptr attributeMouseIn = attribute->dynamicAttribute(KTextEditor::Attribute::ActivateMouseIn)) { if (rangesMouseIn->contains(kateRange)) { attribute = attributeMouseIn; } } // check caret in if (KTextEditor::Attribute::Ptr attributeCaretIn = attribute->dynamicAttribute(KTextEditor::Attribute::ActivateCaretIn)) { if (rangesCaretIn->contains(kateRange)) { attribute = attributeCaretIn; } } } // span range renderRanges.pushNewRange().addRange(*kateRange, attribute); } } // Add selection highlighting if we're creating the selection decorations if ((m_view && selectionsOnly && showSelections() && m_view->selection()) || (completionHighlight && completionSelected) || (m_view && m_view->blockSelection())) { auto ¤tRange = renderRanges.pushNewRange(); // Set up the selection background attribute TODO: move this elsewhere, eg. into the config? static KTextEditor::Attribute::Ptr backgroundAttribute; if (!backgroundAttribute) { backgroundAttribute = KTextEditor::Attribute::Ptr(new KTextEditor::Attribute()); } backgroundAttribute->setBackground(config()->selectionColor()); backgroundAttribute->setForeground(attribute(KTextEditor::dsNormal)->selectedForeground().color()); // Create a range for the current selection if (completionHighlight && completionSelected) { currentRange.addRange(KTextEditor::Range(line, 0, line + 1, 0), backgroundAttribute); } else if (m_view->blockSelection() && m_view->selectionRange().overlapsLine(line)) { currentRange.addRange(m_doc->rangeOnLine(m_view->selectionRange(), line), backgroundAttribute); } else { currentRange.addRange(m_view->selectionRange(), backgroundAttribute); } } // no render ranges, nothing to do, else we loop below endless! if (renderRanges.isEmpty()) { return QVector(); } // Calculate the range which we need to iterate in order to get the highlighting for just this line KTextEditor::Cursor currentPosition, endPosition; if (m_view && selectionsOnly) { if (m_view->blockSelection()) { KTextEditor::Range subRange = m_doc->rangeOnLine(m_view->selectionRange(), line); currentPosition = subRange.start(); endPosition = subRange.end(); } else { KTextEditor::Range rangeNeeded = m_view->selectionRange() & KTextEditor::Range(line, 0, line + 1, 0); currentPosition = qMax(KTextEditor::Cursor(line, 0), rangeNeeded.start()); endPosition = qMin(KTextEditor::Cursor(line + 1, 0), rangeNeeded.end()); } } else { currentPosition = KTextEditor::Cursor(line, 0); endPosition = KTextEditor::Cursor(line + 1, 0); } // Main iterative loop. This walks through each set of highlighting ranges, and stops each // time the highlighting changes. It then creates the corresponding QTextLayout::FormatRanges. QVector newHighlight; while (currentPosition < endPosition) { renderRanges.advanceTo(currentPosition); if (!renderRanges.hasAttribute()) { // No attribute, don't need to create a FormatRange for this text range currentPosition = renderRanges.nextBoundary(); continue; } KTextEditor::Cursor nextPosition = renderRanges.nextBoundary(); // Create the format range and populate with the correct start, length and format info QTextLayout::FormatRange fr; fr.start = currentPosition.column(); if (nextPosition < endPosition || endPosition.line() <= line) { fr.length = nextPosition.column() - currentPosition.column(); } else { // +1 to force background drawing at the end of the line when it's warranted fr.length = textLine->length() - currentPosition.column() + 1; } KTextEditor::Attribute::Ptr a = renderRanges.generateAttribute(); if (a) { fr.format = *a; if (selectionsOnly) { assignSelectionBrushesFromAttribute(fr, *a); } } newHighlight.append(fr); currentPosition = nextPosition; } return newHighlight; } void KateRenderer::assignSelectionBrushesFromAttribute(QTextLayout::FormatRange &target, const KTextEditor::Attribute &attribute) const { if (attribute.hasProperty(SelectedForeground)) { target.format.setForeground(attribute.selectedForeground()); } if (attribute.hasProperty(SelectedBackground)) { target.format.setBackground(attribute.selectedBackground()); } } void KateRenderer::paintTextLine(QPainter &paint, KateLineLayoutPtr range, int xStart, int xEnd, const KTextEditor::Cursor *cursor, PaintTextLineFlags flags) { Q_ASSERT(range->isValid()); // qCDebug(LOG_KTE)<<"KateRenderer::paintTextLine"; // font data const QFontMetricsF &fm = m_fontMetrics; int currentViewLine = -1; if (cursor && cursor->line() == range->line()) { currentViewLine = range->viewLineForColumn(cursor->column()); } paintTextLineBackground(paint, range, currentViewLine, xStart, xEnd); // Draws the dashed underline at the start of a folded block of text. if (!(flags & SkipDrawFirstInvisibleLineUnderlined) && range->startsInvisibleBlock()) { QPen pen(config()->foldingColor()); pen.setCosmetic(true); pen.setStyle(Qt::DashLine); pen.setDashOffset(xStart); pen.setWidth(2); paint.setPen(pen); paint.drawLine(0, (lineHeight() * range->viewLineCount()) - 2, xEnd - xStart, (lineHeight() * range->viewLineCount()) - 2); } if (range->layout()) { bool drawSelection = m_view && m_view->selection() && showSelections() && m_view->selectionRange().overlapsLine(range->line()); // Draw selection in block selection mode. We need 2 kinds of selections that QTextLayout::draw can't render: // - past-end-of-line selection and // - 0-column-wide selection (used to indicate where text will be typed) if (drawSelection && m_view->blockSelection()) { int selectionStartColumn = m_doc->fromVirtualColumn(range->line(), m_doc->toVirtualColumn(m_view->selectionRange().start())); int selectionEndColumn = m_doc->fromVirtualColumn(range->line(), m_doc->toVirtualColumn(m_view->selectionRange().end())); QBrush selectionBrush = config()->selectionColor(); if (selectionStartColumn != selectionEndColumn) { KateTextLayout lastLine = range->viewLine(range->viewLineCount() - 1); if (selectionEndColumn > lastLine.startCol()) { int selectionStartX = (selectionStartColumn > lastLine.startCol()) ? cursorToX(lastLine, selectionStartColumn, true) : 0; int selectionEndX = cursorToX(lastLine, selectionEndColumn, true); paint.fillRect(QRect(selectionStartX - xStart, (int)lastLine.lineLayout().y(), selectionEndX - selectionStartX, lineHeight()), selectionBrush); } } else { const int selectStickWidth = 2; KateTextLayout selectionLine = range->viewLine(range->viewLineForColumn(selectionStartColumn)); int selectionX = cursorToX(selectionLine, selectionStartColumn, true); paint.fillRect(QRect(selectionX - xStart, (int)selectionLine.lineLayout().y(), selectStickWidth, lineHeight()), selectionBrush); } } QVector additionalFormats; if (range->length() > 0) { // We may have changed the pen, be absolutely sure it gets set back to // normal foreground color before drawing text for text that does not // set the pen color paint.setPen(attribute(KTextEditor::dsNormal)->foreground().color()); // Draw the text :) if (drawSelection) { additionalFormats = decorationsForLine(range->textLine(), range->line(), true); range->layout()->draw(&paint, QPoint(-xStart, 0), additionalFormats); } else { range->layout()->draw(&paint, QPoint(-xStart, 0)); } } QBrush backgroundBrush; bool backgroundBrushSet = false; // Loop each individual line for additional text decoration etc. QVectorIterator it = range->layout()->formats(); QVectorIterator it2 = additionalFormats; for (int i = 0; i < range->viewLineCount(); ++i) { KateTextLayout line = range->viewLine(i); bool haveBackground = false; // Determine the background to use, if any, for the end of this view line backgroundBrushSet = false; while (it2.hasNext()) { const QTextLayout::FormatRange &fr = it2.peekNext(); if (fr.start > line.endCol()) { break; } if (fr.start + fr.length > line.endCol()) { if (fr.format.hasProperty(QTextFormat::BackgroundBrush)) { backgroundBrushSet = true; backgroundBrush = fr.format.background(); } haveBackground = true; break; } it2.next(); } while (!haveBackground && it.hasNext()) { const QTextLayout::FormatRange &fr = it.peekNext(); if (fr.start > line.endCol()) { break; } if (fr.start + fr.length > line.endCol()) { if (fr.format.hasProperty(QTextFormat::BackgroundBrush)) { backgroundBrushSet = true; backgroundBrush = fr.format.background(); } break; } it.next(); } // Draw selection or background color outside of areas where text is rendered if (!m_printerFriendly) { bool draw = false; QBrush drawBrush; if (m_view && m_view->selection() && !m_view->blockSelection() && m_view->lineEndSelected(line.end(true))) { draw = true; drawBrush = config()->selectionColor(); } else if (backgroundBrushSet && !m_view->blockSelection()) { draw = true; drawBrush = backgroundBrush; } if (draw) { int fillStartX = line.endX() - line.startX() + line.xOffset() - xStart; int fillStartY = lineHeight() * i; int width = xEnd - xStart - fillStartX; int height = lineHeight(); // reverse X for right-aligned lines if (range->layout()->textOption().alignment() == Qt::AlignRight) { fillStartX = 0; } if (width > 0) { QRect area(fillStartX, fillStartY, width, height); paint.fillRect(area, drawBrush); } } // Draw indent lines if (showIndentLines() && i == 0) { const qreal w = spaceWidth(); const int lastIndentColumn = range->textLine()->indentDepth(m_tabWidth); for (int x = m_indentWidth; x < lastIndentColumn; x += m_indentWidth) { paintIndentMarker(paint, x * w + 1 - xStart, range->line()); } } } // draw an open box to mark non-breaking spaces const QString &text = range->textLine()->string(); int y = lineHeight() * i + fm.ascent() - fm.strikeOutPos(); int nbSpaceIndex = text.indexOf(nbSpaceChar, line.lineLayout().xToCursor(xStart)); while (nbSpaceIndex != -1 && nbSpaceIndex < line.endCol()) { int x = line.lineLayout().cursorToX(nbSpaceIndex); if (x > xEnd) { break; } paintNonBreakSpace(paint, x - xStart, y); nbSpaceIndex = text.indexOf(nbSpaceChar, nbSpaceIndex + 1); } // draw tab stop indicators if (showTabs()) { int tabIndex = text.indexOf(tabChar, line.lineLayout().xToCursor(xStart)); while (tabIndex != -1 && tabIndex < line.endCol()) { int x = line.lineLayout().cursorToX(tabIndex); if (x > xEnd) { break; } paintTabstop(paint, x - xStart + spaceWidth() / 2.0, y); tabIndex = text.indexOf(tabChar, tabIndex + 1); } } // draw trailing spaces if (showSpaces() != KateDocumentConfig::None) { int spaceIndex = line.endCol() - 1; const int trailingPos = showSpaces() == KateDocumentConfig::All ? 0 : qMax(range->textLine()->lastChar(), 0); if (spaceIndex >= trailingPos) { for (; spaceIndex >= line.startCol(); --spaceIndex) { if (!text.at(spaceIndex).isSpace()) { if (showSpaces() == KateDocumentConfig::Trailing) break; else continue; } if (text.at(spaceIndex) != QLatin1Char('\t') || !showTabs()) { if (range->layout()->textOption().alignment() == Qt::AlignRight) { // Draw on left for RTL lines paintSpace(paint, line.lineLayout().cursorToX(spaceIndex) - xStart - spaceWidth() / 2.0, y); } else { paintSpace(paint, line.lineLayout().cursorToX(spaceIndex) - xStart + spaceWidth() / 2.0, y); } } } } } if (showNonPrintableSpaces()) { const int y = lineHeight() * i + fm.ascent(); static const QRegularExpression nonPrintableSpacesRegExp(QStringLiteral("[\\x{2000}-\\x{200F}\\x{2028}-\\x{202F}\\x{205F}-\\x{2064}\\x{206A}-\\x{206F}]")); QRegularExpressionMatchIterator i = nonPrintableSpacesRegExp.globalMatch(text, line.lineLayout().xToCursor(xStart)); while (i.hasNext()) { const int charIndex = i.next().capturedStart(); const int x = line.lineLayout().cursorToX(charIndex); if (x > xEnd) { break; } paintNonPrintableSpaces(paint, x - xStart, y, text[charIndex]); } } } // Draw inline notes if (!isPrinterFriendly()) { const auto inlineNotes = m_view->inlineNotes(range->line()); for (const auto &inlineNoteData : inlineNotes) { KTextEditor::InlineNote inlineNote(inlineNoteData); const int column = inlineNote.position().column(); int viewLine = range->viewLineForColumn(column); // Determine the position where to paint the note. // We start by getting the x coordinate of cursor placed to the column. qreal x = range->viewLine(viewLine).lineLayout().cursorToX(column) - xStart; int textLength = range->length(); if (column == 0 || column < textLength) { // If the note is inside text or at the beginning, then there is a hole in the text where the // note should be painted and the cursor gets placed at the right side of it. So we have to // subtract the width of the note to get to left side of the hole. x -= inlineNote.width(); } else { // If the note is outside the text, then the X coordinate is located at the end of the line. // Add appropriate amount of spaces to reach the required column. x += spaceWidth() * (column - textLength); } qreal y = lineHeight() * viewLine; // Paint the note paint.save(); paint.translate(x, y); inlineNote.provider()->paintInlineNote(inlineNote, paint); paint.restore(); } } // draw word-wrap-honor-indent filling if ((range->viewLineCount() > 1) && range->shiftX() && (range->shiftX() > xStart)) { if (backgroundBrushSet) paint.fillRect(0, lineHeight(), range->shiftX() - xStart, lineHeight() * (range->viewLineCount() - 1), backgroundBrush); paint.fillRect(0, lineHeight(), range->shiftX() - xStart, lineHeight() * (range->viewLineCount() - 1), QBrush(config()->wordWrapMarkerColor(), Qt::Dense4Pattern)); } // Draw caret if (drawCaret() && cursor && range->includesCursor(*cursor)) { int caretWidth, lineWidth = 2; QColor color; QTextLine line = range->layout()->lineForTextPosition(qMin(cursor->column(), range->length())); // Determine the caret's style caretStyles style = caretStyle(); // Make the caret the desired width if (style == Line) { caretWidth = lineWidth; } else if (line.isValid() && cursor->column() < range->length()) { caretWidth = int(line.cursorToX(cursor->column() + 1) - line.cursorToX(cursor->column())); if (caretWidth < 0) { caretWidth = -caretWidth; } } else { caretWidth = spaceWidth(); } // Determine the color if (m_caretOverrideColor.isValid()) { // Could actually use the real highlighting system for this... // would be slower, but more accurate for corner cases color = m_caretOverrideColor; } else { // search for the FormatRange that includes the cursor const auto formatRanges = range->layout()->formats(); for (const QTextLayout::FormatRange &r : formatRanges) { if ((r.start <= cursor->column()) && ((r.start + r.length) > cursor->column())) { // check for Qt::NoBrush, as the returned color is black() and no invalid QColor QBrush foregroundBrush = r.format.foreground(); if (foregroundBrush != Qt::NoBrush) { color = r.format.foreground().color(); } break; } } // still no color found, fall back to default style if (!color.isValid()) { color = attribute(KTextEditor::dsNormal)->foreground().color(); } } paint.save(); switch (style) { case Line: paint.setPen(QPen(color, caretWidth)); break; case Block: // use a gray caret so it's possible to see the character color.setAlpha(128); paint.setPen(QPen(color, caretWidth)); break; case Underline: break; case Half: color.setAlpha(128); paint.setPen(QPen(color, caretWidth)); break; } if (cursor->column() <= range->length()) { range->layout()->drawCursor(&paint, QPoint(-xStart, 0), cursor->column(), caretWidth); } else { // Off the end of the line... must be block mode. Draw the caret ourselves. const KateTextLayout &lastLine = range->viewLine(range->viewLineCount() - 1); int x = cursorToX(lastLine, KTextEditor::Cursor(range->line(), cursor->column()), true); if ((x >= xStart) && (x <= xEnd)) { paint.fillRect(x - xStart, (int)lastLine.lineLayout().y(), caretWidth, lineHeight(), color); } } paint.restore(); } } // show word wrap marker if desirable if ((!isPrinterFriendly()) && config()->wordWrapMarker()) { const QPainter::RenderHints backupRenderHints = paint.renderHints(); paint.setPen(config()->wordWrapMarkerColor()); int _x = qreal(m_doc->config()->wordWrapAt()) * fm.horizontalAdvance(QLatin1Char('x')) - xStart; paint.drawLine(_x, 0, _x, lineHeight()); paint.setRenderHints(backupRenderHints); } } uint KateRenderer::fontHeight() const { return m_fontHeight; } uint KateRenderer::documentHeight() const { return m_doc->lines() * lineHeight(); } int KateRenderer::lineHeight() const { return fontHeight(); } bool KateRenderer::getSelectionBounds(int line, int lineLength, int &start, int &end) const { bool hasSel = false; if (m_view->selection() && !m_view->blockSelection()) { if (m_view->lineIsSelection(line)) { start = m_view->selectionRange().start().column(); end = m_view->selectionRange().end().column(); hasSel = true; } else if (line == m_view->selectionRange().start().line()) { start = m_view->selectionRange().start().column(); end = lineLength; hasSel = true; } else if (m_view->selectionRange().containsLine(line)) { start = 0; end = lineLength; hasSel = true; } else if (line == m_view->selectionRange().end().line()) { start = 0; end = m_view->selectionRange().end().column(); hasSel = true; } } else if (m_view->lineHasSelected(line)) { start = m_view->selectionRange().start().column(); end = m_view->selectionRange().end().column(); hasSel = true; } if (start > end) { int temp = end; end = start; start = temp; } return hasSel; } void KateRenderer::updateConfig() { // update the attribute list pointer updateAttributes(); // update font height, do this before we update the view! updateFontHeight(); // trigger view update, if any! if (m_view) { m_view->updateRendererConfig(); } } void KateRenderer::updateFontHeight() { /** * cache font + metrics */ m_font = config()->baseFont(); m_fontMetrics = QFontMetricsF(m_font); /** * ensure minimal height of one pixel to not fall in the div by 0 trap somewhere * * use a line spacing that matches the code in qt to layout/paint text * * see bug 403868 * https://github.com/qt/qtbase/blob/5.12/src/gui/text/qtextlayout.cpp (line 2270 at the moment) where the text height is set as: * * qreal height = maxY + fontHeight - minY; * * with fontHeight: * * qreal fontHeight = font.ascent() + font.descent(); */ m_fontHeight = qMax(1, qCeil(m_fontMetrics.ascent() + m_fontMetrics.descent())); } void KateRenderer::updateMarkerSize() { // marker size will be calculated over the value defined // on dialog m_markerSize = spaceWidth() / (3.5 - (m_doc->config()->markerSize() * 0.5)); } qreal KateRenderer::spaceWidth() const { return m_fontMetrics.horizontalAdvance(spaceChar); } void KateRenderer::layoutLine(KateLineLayoutPtr lineLayout, int maxwidth, bool cacheLayout) const { // if maxwidth == -1 we have no wrap Kate::TextLine textLine = lineLayout->textLine(); Q_ASSERT(textLine); QTextLayout *l = lineLayout->layout(); if (!l) { l = new QTextLayout(textLine->string(), m_font); } else { l->setText(textLine->string()); l->setFont(m_font); } l->setCacheEnabled(cacheLayout); // Initial setup of the QTextLayout. // Tab width QTextOption opt; opt.setFlags(QTextOption::IncludeTrailingSpaces); opt.setTabStopDistance(m_tabWidth * m_fontMetrics.horizontalAdvance(spaceChar)); if (m_view->config()->dynWrapAnywhere()) { opt.setWrapMode(QTextOption::WrapAnywhere); } else { opt.setWrapMode(QTextOption::WrapAtWordBoundaryOrAnywhere); } // Find the first strong character in the string. // If it is an RTL character, set the base layout direction of the string to RTL. // - // See http://www.unicode.org/reports/tr9/#The_Paragraph_Level (Sections P2 & P3). + // See https://www.unicode.org/reports/tr9/#The_Paragraph_Level (Sections P2 & P3). // Qt's text renderer ("scribe") version 4.2 assumes a "higher-level protocol" // (such as KatePart) will specify the paragraph level, so it does not apply P2 & P3 // by itself. If this ever change in Qt, the next code block could be removed. if (isLineRightToLeft(lineLayout)) { opt.setAlignment(Qt::AlignRight); opt.setTextDirection(Qt::RightToLeft); } else { opt.setAlignment(Qt::AlignLeft); opt.setTextDirection(Qt::LeftToRight); } l->setTextOption(opt); // Syntax highlighting, inbuilt and arbitrary QVector decorations = decorationsForLine(textLine, lineLayout->line()); int firstLineOffset = 0; if (!isPrinterFriendly()) { const auto inlineNotes = m_view->inlineNotes(lineLayout->line()); for (const KTextEditor::InlineNote &inlineNote : inlineNotes) { const int column = inlineNote.position().column(); int width = inlineNote.width(); // Make space for every inline note. // If it is on column 0 (at the beginning of the line), we must offset the first line. // If it is inside the text, we use absolute letter spacing to create space for it between the two letters. // If it is outside of the text, we don't have to make space for it. if (column == 0) { firstLineOffset = width; } else if (column < l->text().length()) { QTextCharFormat text_char_format; text_char_format.setFontLetterSpacing(width); text_char_format.setFontLetterSpacingType(QFont::AbsoluteSpacing); decorations.append(QTextLayout::FormatRange {column - 1, 1, text_char_format}); } } } l->setFormats(decorations); // Begin layouting l->beginLayout(); int height = 0; int shiftX = 0; bool needShiftX = (maxwidth != -1) && m_view && (m_view->config()->dynWordWrapAlignIndent() > 0); forever { QTextLine line = l->createLine(); if (!line.isValid()) { break; } if (maxwidth > 0) { line.setLineWidth(maxwidth); } // we include the leading, this must match the ::updateFontHeight code! line.setLeadingIncluded(true); line.setPosition(QPoint(line.lineNumber() ? shiftX : firstLineOffset, height)); if (needShiftX && line.width() > 0) { needShiftX = false; // Determine x offset for subsequent-lines-of-paragraph indenting int pos = textLine->nextNonSpaceChar(0); if (pos > 0) { shiftX = (int)line.cursorToX(pos); } // check for too deep shift value and limit if necessary if (shiftX > ((double)maxwidth / 100 * m_view->config()->dynWordWrapAlignIndent())) { shiftX = 0; } // if shiftX > 0, the maxwidth has to adapted maxwidth -= shiftX; lineLayout->setShiftX(shiftX); } height += lineHeight(); } l->endLayout(); lineLayout->setLayout(l); } // 1) QString::isRightToLeft() sux // 2) QString::isRightToLeft() is marked as internal (WTF?) // 3) QString::isRightToLeft() does not seem to work on my setup // 4) isStringRightToLeft() should behave much better than QString::isRightToLeft() therefore: // 5) isStringRightToLeft() kicks ass bool KateRenderer::isLineRightToLeft(KateLineLayoutPtr lineLayout) const { QString s = lineLayout->textLine()->string(); int i = 0; // borrowed from QString::updateProperties() while (i != s.length()) { QChar c = s.at(i); switch (c.direction()) { case QChar::DirL: case QChar::DirLRO: case QChar::DirLRE: return false; case QChar::DirR: case QChar::DirAL: case QChar::DirRLO: case QChar::DirRLE: return true; default: break; } i++; } return false; #if 0 // or should we use the direction of the widget? QWidget *display = qobject_cast(view()->parent()); if (!display) { return false; } return display->layoutDirection() == Qt::RightToLeft; #endif } int KateRenderer::cursorToX(const KateTextLayout &range, int col, bool returnPastLine) const { return cursorToX(range, KTextEditor::Cursor(range.line(), col), returnPastLine); } int KateRenderer::cursorToX(const KateTextLayout &range, const KTextEditor::Cursor &pos, bool returnPastLine) const { Q_ASSERT(range.isValid()); int x; if (range.lineLayout().width() > 0) { x = (int)range.lineLayout().cursorToX(pos.column()); } else { x = 0; } int over = pos.column() - range.endCol(); if (returnPastLine && over > 0) { x += over * spaceWidth(); } return x; } KTextEditor::Cursor KateRenderer::xToCursor(const KateTextLayout &range, int x, bool returnPastLine) const { Q_ASSERT(range.isValid()); KTextEditor::Cursor ret(range.line(), range.lineLayout().xToCursor(x)); // TODO wrong for RTL lines? if (returnPastLine && range.endCol(true) == -1 && x > range.width() + range.xOffset()) { ret.setColumn(ret.column() + ((x - (range.width() + range.xOffset())) / spaceWidth())); } return ret; } void KateRenderer::setCaretOverrideColor(const QColor &color) { m_caretOverrideColor = color; } diff --git a/src/utils/kateglobal.cpp b/src/utils/kateglobal.cpp index ce08d37d..af6e96b4 100644 --- a/src/utils/kateglobal.cpp +++ b/src/utils/kateglobal.cpp @@ -1,560 +1,560 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later Copyright (C) 2001-2010 Christoph Cullmann Copyright (C) 2009 Erlend Hamberg This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateglobal.h" #include "config.h" #include #include "katebuffer.h" #include "katecmd.h" #include "katecmds.h" #include "kateconfig.h" #include "katedefaultcolors.h" #include "katedocument.h" #include "katehighlightingcmds.h" #include "katekeywordcompletion.h" #include "katemodemanager.h" #include "katepartdebug.h" #include "katerenderer.h" #include "kateschema.h" #include "kateschemaconfig.h" #include "katescriptmanager.h" #include "katesedcmd.h" #include "katevariableexpansionmanager.h" #include "kateview.h" #include "katewordcompletion.h" #include "spellcheck/spellcheck.h" #include "katenormalinputmodefactory.h" #include "kateviinputmodefactory.h" #include #include #include #include #include #include #include #include #include #include #include #if LIBGIT2_FOUND #include #endif // BEGIN unit test mode static bool kateUnitTestMode = false; void KTextEditor::EditorPrivate::enableUnitTestMode() { kateUnitTestMode = true; } bool KTextEditor::EditorPrivate::unitTestMode() { return kateUnitTestMode; } // END unit test mode KTextEditor::EditorPrivate::EditorPrivate(QPointer &staticInstance) : KTextEditor::Editor(this) , m_aboutData(QStringLiteral("katepart"), i18n("Kate Part"), QStringLiteral(KTEXTEDITOR_VERSION_STRING), i18n("Embeddable editor component"), KAboutLicense::LGPL_V2, i18n("(c) 2000-2019 The Kate Authors"), QString(), QStringLiteral("https://kate-editor.org")) , m_dummyApplication(nullptr) , m_application(&m_dummyApplication) , m_dummyMainWindow(nullptr) , m_defaultColors(new KateDefaultColors()) , m_searchHistoryModel(nullptr) , m_replaceHistoryModel(nullptr) { // remember this staticInstance = this; // init libgit2, we require at least 0.22 which has this function! #if LIBGIT2_FOUND git_libgit2_init(); #endif /** * register some datatypes */ qRegisterMetaType("KTextEditor::Cursor"); qRegisterMetaType("KTextEditor::Document*"); qRegisterMetaType("KTextEditor::View*"); // // fill about data // m_aboutData.addAuthor(i18n("Christoph Cullmann"), i18n("Maintainer"), QStringLiteral("cullmann@kde.org"), QStringLiteral("https://cullmann.io")); m_aboutData.addAuthor(i18n("Dominik Haumann"), i18n("Core Developer"), QStringLiteral("dhaumann@kde.org")); - m_aboutData.addAuthor(i18n("Milian Wolff"), i18n("Core Developer"), QStringLiteral("mail@milianw.de"), QStringLiteral("http://milianw.de")); + m_aboutData.addAuthor(i18n("Milian Wolff"), i18n("Core Developer"), QStringLiteral("mail@milianw.de"), QStringLiteral("https://milianw.de/")); m_aboutData.addAuthor(i18n("Joseph Wenninger"), i18n("Core Developer"), QStringLiteral("jowenn@kde.org"), QStringLiteral("http://stud3.tuwien.ac.at/~e9925371")); m_aboutData.addAuthor(i18n("Erlend Hamberg"), i18n("Vi Input Mode"), QStringLiteral("ehamberg@gmail.com"), QStringLiteral("https://hamberg.no/erlend")); m_aboutData.addAuthor(i18n("Bernhard Beschow"), i18n("Developer"), QStringLiteral("bbeschow@cs.tu-berlin.de"), QStringLiteral("https://user.cs.tu-berlin.de/~bbeschow")); m_aboutData.addAuthor(i18n("Anders Lund"), i18n("Core Developer"), QStringLiteral("anders@alweb.dk"), QStringLiteral("https://alweb.dk")); m_aboutData.addAuthor(i18n("Michel Ludwig"), i18n("On-the-fly spell checking"), QStringLiteral("michel.ludwig@kdemail.net")); m_aboutData.addAuthor(i18n("Pascal Létourneau"), i18n("Large scale bug fixing"), QStringLiteral("pascal.letourneau@gmail.com")); m_aboutData.addAuthor(i18n("Hamish Rodda"), i18n("Core Developer"), QStringLiteral("rodda@kde.org")); m_aboutData.addAuthor(i18n("Waldo Bastian"), i18n("The cool buffersystem"), QStringLiteral("bastian@kde.org")); m_aboutData.addAuthor(i18n("Charles Samuels"), i18n("The Editing Commands"), QStringLiteral("charles@kde.org")); m_aboutData.addAuthor(i18n("Matt Newell"), i18n("Testing, ..."), QStringLiteral("newellm@proaxis.com")); m_aboutData.addAuthor(i18n("Michael Bartl"), i18n("Former Core Developer"), QStringLiteral("michael.bartl1@chello.at")); m_aboutData.addAuthor(i18n("Michael McCallum"), i18n("Core Developer"), QStringLiteral("gholam@xtra.co.nz")); m_aboutData.addAuthor(i18n("Michael Koch"), i18n("KWrite port to KParts"), QStringLiteral("koch@kde.org")); m_aboutData.addAuthor(i18n("Christian Gebauer"), QString(), QStringLiteral("gebauer@kde.org")); m_aboutData.addAuthor(i18n("Simon Hausmann"), QString(), QStringLiteral("hausmann@kde.org")); m_aboutData.addAuthor(i18n("Glen Parker"), i18n("KWrite Undo History, Kspell integration"), QStringLiteral("glenebob@nwlink.com")); m_aboutData.addAuthor(i18n("Scott Manson"), i18n("KWrite XML Syntax highlighting support"), QStringLiteral("sdmanson@alltel.net")); m_aboutData.addAuthor(i18n("John Firebaugh"), i18n("Patches and more"), QStringLiteral("jfirebaugh@kde.org")); m_aboutData.addAuthor(i18n("Andreas Kling"), i18n("Developer"), QStringLiteral("kling@impul.se")); m_aboutData.addAuthor(i18n("Mirko Stocker"), i18n("Various bugfixes"), QStringLiteral("me@misto.ch"), QStringLiteral("https://misto.ch/")); m_aboutData.addAuthor(i18n("Matthew Woehlke"), i18n("Selection, KColorScheme integration"), QStringLiteral("mw_triad@users.sourceforge.net")); m_aboutData.addAuthor(i18n("Sebastian Pipping"), i18n("Search bar back- and front-end"), QStringLiteral("webmaster@hartwork.org"), QStringLiteral("https://hartwork.org/")); m_aboutData.addAuthor(i18n("Jochen Wilhelmy"), i18n("Original KWrite Author"), QStringLiteral("digisnap@cs.tu-berlin.de")); m_aboutData.addAuthor(i18n("Gerald Senarclens de Grancy"), i18n("QA and Scripting"), QStringLiteral("oss@senarclens.eu"), QStringLiteral("http://find-santa.eu/")); m_aboutData.addCredit(i18n("Matteo Merli"), i18n("Highlighting for RPM Spec-Files, Perl, Diff and more"), QStringLiteral("merlim@libero.it")); m_aboutData.addCredit(i18n("Rocky Scaletta"), i18n("Highlighting for VHDL"), QStringLiteral("rocky@purdue.edu")); m_aboutData.addCredit(i18n("Yury Lebedev"), i18n("Highlighting for SQL"), QString()); m_aboutData.addCredit(i18n("Chris Ross"), i18n("Highlighting for Ferite"), QString()); m_aboutData.addCredit(i18n("Nick Roux"), i18n("Highlighting for ILERPG"), QString()); m_aboutData.addCredit(i18n("Carsten Niehaus"), i18n("Highlighting for LaTeX"), QString()); m_aboutData.addCredit(i18n("Per Wigren"), i18n("Highlighting for Makefiles, Python"), QString()); m_aboutData.addCredit(i18n("Jan Fritz"), i18n("Highlighting for Python"), QString()); m_aboutData.addCredit(i18n("Daniel Naber")); m_aboutData.addCredit(i18n("Roland Pabel"), i18n("Highlighting for Scheme"), QString()); m_aboutData.addCredit(i18n("Cristi Dumitrescu"), i18n("PHP Keyword/Datatype list"), QString()); m_aboutData.addCredit(i18n("Carsten Pfeiffer"), i18n("Very nice help"), QString()); m_aboutData.addCredit(i18n("Bruno Massa"), i18n("Highlighting for Lua"), QStringLiteral("brmassa@gmail.com")); m_aboutData.addCredit(i18n("All people who have contributed and I have forgotten to mention")); m_aboutData.setTranslator(i18nc("NAME OF TRANSLATORS", "Your names"), i18nc("EMAIL OF TRANSLATORS", "Your emails")); /** * set proper Kate icon for our about dialog */ m_aboutData.setProgramLogo(QIcon(QStringLiteral(":/ktexteditor/kate.svg"))); // // dir watch // m_dirWatch = new KDirWatch(); // // command manager // m_cmdManager = new KateCmd(); // // variable expansion manager // m_variableExpansionManager = new KateVariableExpansionManager(this); // // hl manager // m_hlManager = new KateHlManager(); // // mode man // m_modeManager = new KateModeManager(); // // schema man // m_schemaManager = new KateSchemaManager(); // // input mode factories // KateAbstractInputModeFactory *fact; fact = new KateNormalInputModeFactory(); m_inputModeFactories.insert(KTextEditor::View::NormalInputMode, fact); #if BUILD_VIMODE fact = new KateViInputModeFactory(); m_inputModeFactories.insert(KTextEditor::View::ViInputMode, fact); #endif // // spell check manager // m_spellCheckManager = new KateSpellCheckManager(); // config objects m_globalConfig = new KateGlobalConfig(); m_documentConfig = new KateDocumentConfig(); m_viewConfig = new KateViewConfig(); m_rendererConfig = new KateRendererConfig(); // create script manager (search scripts) m_scriptManager = KateScriptManager::self(); // // init the cmds // m_cmds.push_back(KateCommands::CoreCommands::self()); m_cmds.push_back(KateCommands::Character::self()); m_cmds.push_back(KateCommands::Date::self()); m_cmds.push_back(KateCommands::SedReplace::self()); m_cmds.push_back(KateCommands::Highlighting::self()); // global word completion model m_wordCompletionModel = new KateWordCompletionModel(this); // global keyword completion model m_keywordCompletionModel = new KateKeywordCompletionModel(this); // tap to QApplication object for color palette changes qApp->installEventFilter(this); } KTextEditor::EditorPrivate::~EditorPrivate() { delete m_globalConfig; delete m_documentConfig; delete m_viewConfig; delete m_rendererConfig; delete m_modeManager; delete m_schemaManager; delete m_dirWatch; // cu managers delete m_scriptManager; delete m_hlManager; delete m_spellCheckManager; // cu model delete m_wordCompletionModel; // delete variable expansion manager delete m_variableExpansionManager; m_variableExpansionManager = nullptr; // delete the commands before we delete the cmd manager qDeleteAll(m_cmds); delete m_cmdManager; qDeleteAll(m_inputModeFactories); // shutdown libgit2, we require at least 0.22 which has this function! #if LIBGIT2_FOUND git_libgit2_shutdown(); #endif } KTextEditor::Document *KTextEditor::EditorPrivate::createDocument(QObject *parent) { KTextEditor::DocumentPrivate *doc = new KTextEditor::DocumentPrivate(false, false, nullptr, parent); emit documentCreated(this, doc); return doc; } // END KTextEditor::Editor config stuff void KTextEditor::EditorPrivate::configDialog(QWidget *parent) { QPointer kd = new KPageDialog(parent); kd->setWindowTitle(i18n("Configure")); kd->setFaceType(KPageDialog::List); kd->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply | QDialogButtonBox::Help); QList editorPages; for (int i = 0; i < configPages(); ++i) { QFrame *page = new QFrame(); KTextEditor::ConfigPage *cp = configPage(i, page); KPageWidgetItem *item = kd->addPage(page, cp->name()); item->setHeader(cp->fullName()); item->setIcon(cp->icon()); QVBoxLayout *topLayout = new QVBoxLayout(page); topLayout->setContentsMargins(0, 0, 0, 0); connect(kd->button(QDialogButtonBox::Apply), SIGNAL(clicked()), cp, SLOT(apply())); topLayout->addWidget(cp); editorPages.append(cp); } if (kd->exec() && kd) { KateGlobalConfig::global()->configStart(); KateDocumentConfig::global()->configStart(); KateViewConfig::global()->configStart(); KateRendererConfig::global()->configStart(); for (int i = 0; i < editorPages.count(); ++i) { editorPages.at(i)->apply(); } KateGlobalConfig::global()->configEnd(); KateDocumentConfig::global()->configEnd(); KateViewConfig::global()->configEnd(); KateRendererConfig::global()->configEnd(); } delete kd; } int KTextEditor::EditorPrivate::configPages() const { return 4; } KTextEditor::ConfigPage *KTextEditor::EditorPrivate::configPage(int number, QWidget *parent) { switch (number) { case 0: return new KateViewDefaultsConfig(parent); case 1: return new KateSchemaConfigPage(parent); case 2: return new KateEditConfigTab(parent); case 3: return new KateSaveConfigTab(parent); default: break; } return nullptr; } /** * Cleanup the KTextEditor::EditorPrivate during QCoreApplication shutdown */ static void cleanupGlobal() { /** * delete if there */ delete KTextEditor::EditorPrivate::self(); } KTextEditor::EditorPrivate *KTextEditor::EditorPrivate::self() { /** * remember the static instance in a QPointer */ static bool inited = false; static QPointer staticInstance; /** * just return it, if already inited */ if (inited) { return staticInstance.data(); } /** * start init process */ inited = true; /** * now create the object and store it */ new KTextEditor::EditorPrivate(staticInstance); /** * register cleanup * let use be deleted during QCoreApplication shutdown */ qAddPostRoutine(cleanupGlobal); /** * return instance */ return staticInstance.data(); } void KTextEditor::EditorPrivate::registerDocument(KTextEditor::DocumentPrivate *doc) { Q_ASSERT(!m_documents.contains(doc)); m_documents.insert(doc, doc); } void KTextEditor::EditorPrivate::deregisterDocument(KTextEditor::DocumentPrivate *doc) { Q_ASSERT(m_documents.contains(doc)); m_documents.remove(doc); } void KTextEditor::EditorPrivate::registerView(KTextEditor::ViewPrivate *view) { Q_ASSERT(!m_views.contains(view)); m_views.insert(view); } void KTextEditor::EditorPrivate::deregisterView(KTextEditor::ViewPrivate *view) { Q_ASSERT(m_views.contains(view)); m_views.remove(view); } KTextEditor::Command *KTextEditor::EditorPrivate::queryCommand(const QString &cmd) const { return m_cmdManager->queryCommand(cmd); } QList KTextEditor::EditorPrivate::commands() const { return m_cmdManager->commands(); } QStringList KTextEditor::EditorPrivate::commandList() const { return m_cmdManager->commandList(); } KateVariableExpansionManager *KTextEditor::EditorPrivate::variableExpansionManager() { return m_variableExpansionManager; } void KTextEditor::EditorPrivate::updateColorPalette() { // update default color cache m_defaultColors.reset(new KateDefaultColors()); // reload the global schema (triggers reload for every view as well) m_rendererConfig->reloadSchema(); // force full update of all view caches and colors m_rendererConfig->updateConfig(); } void KTextEditor::EditorPrivate::copyToClipboard(const QString &text) { /** * empty => nop */ if (text.isEmpty()) { return; } /** * move to clipboard */ QApplication::clipboard()->setText(text, QClipboard::Clipboard); /** * LRU, kill potential duplicated, move new entry to top * cut after 10 entries */ m_clipboardHistory.removeOne(text); m_clipboardHistory.prepend(text); if (m_clipboardHistory.size() > 10) { m_clipboardHistory.removeLast(); } /** * notify about change */ emit clipboardHistoryChanged(); } bool KTextEditor::EditorPrivate::eventFilter(QObject *obj, QEvent *event) { if (obj == qApp && event->type() == QEvent::ApplicationPaletteChange) { // only update the color once for the event that belongs to the qApp updateColorPalette(); } return false; // always continue processing } QList KTextEditor::EditorPrivate::inputModeFactories() { return m_inputModeFactories.values(); } QStringListModel *KTextEditor::EditorPrivate::searchHistoryModel() { if (!m_searchHistoryModel) { KConfigGroup cg(KSharedConfig::openConfig(), "KTextEditor::Search"); const QStringList history = cg.readEntry(QStringLiteral("Search History"), QStringList()); m_searchHistoryModel = new QStringListModel(history, this); } return m_searchHistoryModel; } QStringListModel *KTextEditor::EditorPrivate::replaceHistoryModel() { if (!m_replaceHistoryModel) { KConfigGroup cg(KSharedConfig::openConfig(), "KTextEditor::Search"); const QStringList history = cg.readEntry(QStringLiteral("Replace History"), QStringList()); m_replaceHistoryModel = new QStringListModel(history, this); } return m_replaceHistoryModel; } void KTextEditor::EditorPrivate::saveSearchReplaceHistoryModels() { KConfigGroup cg(KSharedConfig::openConfig(), "KTextEditor::Search"); if (m_searchHistoryModel) { cg.writeEntry(QStringLiteral("Search History"), m_searchHistoryModel->stringList()); } if (m_replaceHistoryModel) { cg.writeEntry(QStringLiteral("Replace History"), m_replaceHistoryModel->stringList()); } } KSharedConfigPtr KTextEditor::EditorPrivate::config() { // use dummy config for unit tests! if (KTextEditor::EditorPrivate::unitTestMode()) { return KSharedConfig::openConfig(QStringLiteral("katepartrc-unittest"), KConfig::SimpleConfig, QStandardPaths::TempLocation); } // else: use application configuration, but try to transfer global settings on first use auto applicationConfig = KSharedConfig::openConfig(); if (!KConfigGroup(applicationConfig, QStringLiteral("KTextEditor Editor")).exists()) { auto globalConfig = KSharedConfig::openConfig(QStringLiteral("katepartrc")); for (const auto &group : {QStringLiteral("Editor"), QStringLiteral("Document"), QStringLiteral("View"), QStringLiteral("Renderer")}) { KConfigGroup origin(globalConfig, group); KConfigGroup destination(applicationConfig, QStringLiteral("KTextEditor ") + group); origin.copyTo(&destination); } } return applicationConfig; } diff --git a/src/view/kateviewhelpers.cpp b/src/view/kateviewhelpers.cpp index c4d61d8d..8b8c87fc 100644 --- a/src/view/kateviewhelpers.cpp +++ b/src/view/kateviewhelpers.cpp @@ -1,3050 +1,3050 @@ /* This file is part of the KDE libraries Copyright (C) 2008, 2009 Matthew Woehlke Copyright (C) 2007 Mirko Stocker Copyright (C) 2002 John Firebaugh Copyright (C) 2001 Anders Lund Copyright (C) 2001 Christoph Cullmann Copyright (C) 2011 Svyatoslav Kuzmich Copyright (C) 2012 Kåre Särs (Minimap) Copyright 2017-2018 Friedrich W. H. Kossebau 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.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kateviewhelpers.h" #include "kateabstractinputmode.h" #include "kateannotationitemdelegate.h" #include "katecmd.h" #include "katecommandrangeexpressionparser.h" #include "kateconfig.h" #include "katedocument.h" #include "kateglobal.h" #include "katelayoutcache.h" #include "katepartdebug.h" #include "katerenderer.h" #include "katetextlayout.h" #include "katetextpreview.h" #include "kateview.h" #include "kateviewinternal.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // BEGIN KateMessageLayout KateMessageLayout::KateMessageLayout(QWidget *parent) : QLayout(parent) { } KateMessageLayout::~KateMessageLayout() { while (QLayoutItem *item = takeAt(0)) delete item; } void KateMessageLayout::addItem(QLayoutItem *item) { Q_ASSERT(false); add(item, KTextEditor::Message::CenterInView); } void KateMessageLayout::addWidget(QWidget *widget, KTextEditor::Message::MessagePosition pos) { add(new QWidgetItem(widget), pos); } int KateMessageLayout::count() const { return m_items.size(); } QLayoutItem *KateMessageLayout::itemAt(int index) const { if (index < 0 || index >= m_items.size()) return nullptr; return m_items[index]->item; } void KateMessageLayout::setGeometry(const QRect &rect) { QLayout::setGeometry(rect); const int s = spacing(); const QRect adjustedRect = rect.adjusted(s, s, -s, -s); for (auto wrapper : m_items) { QLayoutItem *item = wrapper->item; auto position = wrapper->position; if (position == KTextEditor::Message::TopInView) { const QRect r(adjustedRect.width() - item->sizeHint().width(), s, item->sizeHint().width(), item->sizeHint().height()); item->setGeometry(r); } else if (position == KTextEditor::Message::BottomInView) { const QRect r(adjustedRect.width() - item->sizeHint().width(), adjustedRect.height() - item->sizeHint().height(), item->sizeHint().width(), item->sizeHint().height()); item->setGeometry(r); } else if (position == KTextEditor::Message::CenterInView) { QRect r(0, 0, item->sizeHint().width(), item->sizeHint().height()); r.moveCenter(adjustedRect.center()); item->setGeometry(r); } else { Q_ASSERT_X(false, "setGeometry", "Only TopInView, CenterInView, and BottomInView are supported."); } } } QSize KateMessageLayout::sizeHint() const { return QSize(); } QLayoutItem *KateMessageLayout::takeAt(int index) { if (index >= 0 && index < m_items.size()) { ItemWrapper *layoutStruct = m_items.takeAt(index); return layoutStruct->item; } return nullptr; } void KateMessageLayout::add(QLayoutItem *item, KTextEditor::Message::MessagePosition pos) { m_items.push_back(new ItemWrapper(item, pos)); } // END KateMessageLayout // BEGIN KateScrollBar static const int s_lineWidth = 100; static const int s_pixelMargin = 8; static const int s_linePixelIncLimit = 6; const unsigned char KateScrollBar::characterOpacity[256] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 15 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 255, 0, 0, 0, 0, 0, // <- 31 0, 125, 41, 221, 138, 195, 218, 21, 142, 142, 137, 137, 97, 87, 87, 140, // <- 47 223, 164, 183, 190, 191, 193, 214, 158, 227, 216, 103, 113, 146, 140, 146, 149, // <- 63 248, 204, 240, 174, 217, 197, 178, 205, 209, 176, 168, 211, 160, 246, 238, 218, // <- 79 195, 229, 227, 196, 167, 212, 188, 238, 197, 169, 189, 158, 21, 151, 115, 90, // <- 95 15, 192, 209, 153, 208, 187, 162, 221, 183, 149, 161, 191, 146, 203, 167, 182, // <- 111 208, 203, 139, 166, 158, 167, 157, 189, 164, 179, 156, 167, 145, 166, 109, 0, // <- 127 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 143 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // <- 159 0, 125, 184, 187, 146, 201, 127, 203, 89, 194, 156, 141, 117, 87, 202, 88, // <- 175 115, 165, 118, 121, 85, 190, 236, 87, 88, 111, 151, 140, 194, 191, 203, 148, // <- 191 215, 215, 222, 224, 223, 234, 230, 192, 208, 208, 216, 217, 187, 187, 194, 195, // <- 207 228, 255, 228, 228, 235, 239, 237, 150, 255, 222, 222, 229, 232, 180, 197, 225, // <- 223 208, 208, 216, 217, 212, 230, 218, 170, 202, 202, 211, 204, 156, 156, 165, 159, // <- 239 214, 194, 197, 197, 206, 206, 201, 132, 214, 183, 183, 192, 187, 195, 227, 198}; KateScrollBar::KateScrollBar(Qt::Orientation orientation, KateViewInternal *parent) : QScrollBar(orientation, parent->m_view) , m_middleMouseDown(false) , m_leftMouseDown(false) , m_view(parent->m_view) , m_doc(parent->doc()) , m_viewInternal(parent) , m_textPreview(nullptr) , m_showMarks(false) , m_showMiniMap(false) , m_miniMapAll(true) , m_miniMapWidth(40) , m_grooveHeight(height()) , m_linesModified(0) { connect(this, SIGNAL(valueChanged(int)), this, SLOT(sliderMaybeMoved(int))); connect(m_doc, SIGNAL(marksChanged(KTextEditor::Document *)), this, SLOT(marksChanged())); m_updateTimer.setInterval(300); m_updateTimer.setSingleShot(true); QTimer::singleShot(10, this, SLOT(updatePixmap())); // track mouse for text preview widget setMouseTracking(orientation == Qt::Vertical); // setup text preview timer m_delayTextPreviewTimer.setSingleShot(true); m_delayTextPreviewTimer.setInterval(250); connect(&m_delayTextPreviewTimer, SIGNAL(timeout()), this, SLOT(showTextPreview())); } KateScrollBar::~KateScrollBar() { delete m_textPreview; } void KateScrollBar::setShowMiniMap(bool b) { if (b && !m_showMiniMap) { connect(m_view, SIGNAL(selectionChanged(KTextEditor::View *)), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(m_doc, SIGNAL(textChanged(KTextEditor::Document *)), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(m_view, SIGNAL(delayedUpdateOfView()), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); connect(&m_updateTimer, SIGNAL(timeout()), this, SLOT(updatePixmap()), Qt::UniqueConnection); connect(&(m_view->textFolding()), SIGNAL(foldingRangesChanged()), &m_updateTimer, SLOT(start()), Qt::UniqueConnection); } else if (!b) { disconnect(&m_updateTimer); } m_showMiniMap = b; updateGeometry(); update(); } QSize KateScrollBar::sizeHint() const { if (m_showMiniMap) { return QSize(m_miniMapWidth, QScrollBar::sizeHint().height()); } return QScrollBar::sizeHint(); } int KateScrollBar::minimapYToStdY(int y) { // Check if the minimap fills the whole scrollbar if (m_stdGroveRect.height() == m_mapGroveRect.height()) { return y; } // check if y is on the step up/down if ((y < m_stdGroveRect.top()) || (y > m_stdGroveRect.bottom())) { return y; } if (y < m_mapGroveRect.top()) { return m_stdGroveRect.top() + 1; } if (y > m_mapGroveRect.bottom()) { return m_stdGroveRect.bottom() - 1; } // check for div/0 if (m_mapGroveRect.height() == 0) { return y; } int newY = (y - m_mapGroveRect.top()) * m_stdGroveRect.height() / m_mapGroveRect.height(); newY += m_stdGroveRect.top(); return newY; } void KateScrollBar::mousePressEvent(QMouseEvent *e) { // delete text preview hideTextPreview(); if (e->button() == Qt::MidButton) { m_middleMouseDown = true; } else if (e->button() == Qt::LeftButton) { m_leftMouseDown = true; } if (m_showMiniMap) { if (m_leftMouseDown && e->pos().y() > m_mapGroveRect.top() && e->pos().y() < m_mapGroveRect.bottom()) { // if we show the minimap left-click jumps directly to the selected position int newVal = (e->pos().y() - m_mapGroveRect.top()) / (double)m_mapGroveRect.height() * (double)(maximum() + pageStep()) - pageStep() / 2; newVal = qBound(0, newVal, maximum()); setSliderPosition(newVal); } QMouseEvent eMod(QEvent::MouseButtonPress, QPoint(6, minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mousePressEvent(&eMod); } else { QScrollBar::mousePressEvent(e); } m_toolTipPos = e->globalPos() - QPoint(e->pos().x(), 0); const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); redrawMarks(); } void KateScrollBar::mouseReleaseEvent(QMouseEvent *e) { if (e->button() == Qt::MidButton) { m_middleMouseDown = false; } else if (e->button() == Qt::LeftButton) { m_leftMouseDown = false; } redrawMarks(); if (m_leftMouseDown || m_middleMouseDown) { QToolTip::hideText(); } if (m_showMiniMap) { QMouseEvent eMod(QEvent::MouseButtonRelease, QPoint(e->pos().x(), minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mouseReleaseEvent(&eMod); } else { QScrollBar::mouseReleaseEvent(e); } } void KateScrollBar::mouseMoveEvent(QMouseEvent *e) { if (m_showMiniMap) { QMouseEvent eMod(QEvent::MouseMove, QPoint(e->pos().x(), minimapYToStdY(e->pos().y())), e->button(), e->buttons(), e->modifiers()); QScrollBar::mouseMoveEvent(&eMod); } else { QScrollBar::mouseMoveEvent(e); } if (e->buttons() & (Qt::LeftButton | Qt::MidButton)) { redrawMarks(); // current line tool tip m_toolTipPos = e->globalPos() - QPoint(e->pos().x(), 0); const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); } showTextPreviewDelayed(); } void KateScrollBar::leaveEvent(QEvent *event) { hideTextPreview(); QAbstractSlider::leaveEvent(event); } bool KateScrollBar::eventFilter(QObject *object, QEvent *event) { Q_UNUSED(object) if (m_textPreview && event->type() == QEvent::WindowDeactivate) { // We need hide the scrollbar TextPreview widget hideTextPreview(); } return false; } void KateScrollBar::paintEvent(QPaintEvent *e) { if (m_doc->marks().size() != m_lines.size()) { recomputeMarksPositions(); } if (m_showMiniMap) { miniMapPaintEvent(e); } else { normalPaintEvent(e); } } void KateScrollBar::showTextPreviewDelayed() { if (!m_textPreview) { if (!m_delayTextPreviewTimer.isActive()) { m_delayTextPreviewTimer.start(); } } else { showTextPreview(); } } void KateScrollBar::showTextPreview() { if (orientation() != Qt::Vertical || isSliderDown() || (minimum() == maximum()) || !m_view->config()->scrollBarPreview()) { return; } // only show when main window is active (#392396) if (window() && !window()->isActiveWindow()) { return; } QRect grooveRect; if (m_showMiniMap) { // If mini-map is shown, the height of the map might not be the whole height grooveRect = m_mapGroveRect; } else { QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); } if (m_view->config()->scrollPastEnd()) { // Adjust the grove size to accommodate the added pageStep at the bottom int adjust = pageStep() * grooveRect.height() / (maximum() + pageStep() - minimum()); grooveRect.adjust(0, 0, 0, -adjust); } const QPoint cursorPos = mapFromGlobal(QCursor::pos()); if (grooveRect.contains(cursorPos)) { if (!m_textPreview) { m_textPreview = new KateTextPreview(m_view, this); m_textPreview->setAttribute(Qt::WA_ShowWithoutActivating); m_textPreview->setFrameStyle(QFrame::StyledPanel); // event filter to catch application WindowDeactivate event, to hide the preview window qApp->installEventFilter(this); } const qreal posInPercent = static_cast(cursorPos.y() - grooveRect.top()) / grooveRect.height(); const qreal startLine = posInPercent * m_view->textFolding().visibleLines(); m_textPreview->resize(m_view->width() / 2, m_view->height() / 5); const int xGlobal = mapToGlobal(QPoint(0, 0)).x(); const int yGlobal = qMin(mapToGlobal(QPoint(0, height())).y() - m_textPreview->height(), qMax(mapToGlobal(QPoint(0, 0)).y(), mapToGlobal(cursorPos).y() - m_textPreview->height() / 2)); m_textPreview->move(xGlobal - m_textPreview->width(), yGlobal); m_textPreview->setLine(startLine); m_textPreview->setCenterView(true); m_textPreview->setScaleFactor(0.75); m_textPreview->raise(); m_textPreview->show(); } else { hideTextPreview(); } } void KateScrollBar::hideTextPreview() { if (m_delayTextPreviewTimer.isActive()) { m_delayTextPreviewTimer.stop(); } qApp->removeEventFilter(this); delete m_textPreview; } // This function is optimized for bing called in sequence. const QColor KateScrollBar::charColor(const QVector &attributes, int &attributeIndex, const QVector &decorations, const QColor &defaultColor, int x, QChar ch) { QColor color = defaultColor; bool styleFound = false; // Query the decorations, that is, things like search highlighting, or the // KDevelop DUChain highlighting, for a color to use for (auto &range : decorations) { if (range.start <= x && range.start + range.length > x) { // If there's a different background color set (search markers, ...) // use that, otherwise use the foreground color. if (range.format.hasProperty(QTextFormat::BackgroundBrush)) { color = range.format.background().color(); } else { color = range.format.foreground().color(); } styleFound = true; break; } } // If there's no decoration set for the current character (this will mostly be the case for // plain Kate), query the styles, that is, the default kate syntax highlighting. if (!styleFound) { // go to the block containing x while ((attributeIndex < attributes.size()) && ((attributes[attributeIndex].offset + attributes[attributeIndex].length) < x)) { ++attributeIndex; } if ((attributeIndex < attributes.size()) && (x < attributes[attributeIndex].offset + attributes[attributeIndex].length)) { color = m_view->renderer()->attribute(attributes[attributeIndex].attributeValue)->foreground().color(); } } // Query how much "blackness" the character has. // This causes for example a dot or a dash to appear less intense // than an A or similar. // This gives the pixels created a bit of structure, which makes it look more // like real text. color.setAlpha((ch.unicode() < 256) ? characterOpacity[ch.unicode()] : 222); return color; } void KateScrollBar::updatePixmap() { // QTime time; // time.start(); if (!m_showMiniMap) { // make sure no time is wasted if the option is disabled return; } // For performance reason, only every n-th line will be drawn if the widget is // sufficiently small compared to the amount of lines in the document. int docLineCount = m_view->textFolding().visibleLines(); int pixmapLineCount = docLineCount; if (m_view->config()->scrollPastEnd()) { pixmapLineCount += pageStep(); } int pixmapLinesUnscaled = pixmapLineCount; if (m_grooveHeight < 5) { m_grooveHeight = 5; } int lineDivisor = pixmapLinesUnscaled / m_grooveHeight; if (lineDivisor < 1) { lineDivisor = 1; } int charIncrement = 1; int lineIncrement = 1; if ((m_grooveHeight > 10) && (pixmapLineCount >= m_grooveHeight * 2)) { charIncrement = pixmapLineCount / m_grooveHeight; while (charIncrement > s_linePixelIncLimit) { lineIncrement++; pixmapLineCount = pixmapLinesUnscaled / lineIncrement; charIncrement = pixmapLineCount / m_grooveHeight; } pixmapLineCount /= charIncrement; } int pixmapLineWidth = s_pixelMargin + s_lineWidth / charIncrement; // qCDebug(LOG_KTE) << "l" << lineIncrement << "c" << charIncrement << "d" << lineDivisor; // qCDebug(LOG_KTE) << "pixmap" << pixmapLineCount << pixmapLineWidth << "docLines" << m_view->textFolding().visibleLines() << "height" << m_grooveHeight; const QColor backgroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->background().color(); const QColor defaultTextColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); const QColor selectionBgColor = m_view->renderer()->config()->selectionColor(); QColor modifiedLineColor = m_view->renderer()->config()->modifiedLineColor(); QColor savedLineColor = m_view->renderer()->config()->savedLineColor(); // move the modified line color away from the background color modifiedLineColor.setHsv(modifiedLineColor.hue(), 255, 255 - backgroundColor.value() / 3); savedLineColor.setHsv(savedLineColor.hue(), 100, 255 - backgroundColor.value() / 3); // increase dimensions by ratio m_pixmap = QPixmap(pixmapLineWidth * m_view->devicePixelRatioF(), pixmapLineCount * m_view->devicePixelRatioF()); m_pixmap.fill(QColor("transparent")); // The text currently selected in the document, to be drawn later. const KTextEditor::Range &selection = m_view->selectionRange(); QPainter painter; if (painter.begin(&m_pixmap)) { // init pen once, afterwards, only change it if color changes to avoid a lot of allocation for setPen painter.setPen(selectionBgColor); // Do not force updates of the highlighting if the document is very large bool simpleMode = m_doc->lines() > 7500; int pixelY = 0; int drawnLines = 0; // Iterate over all visible lines, drawing them. for (int virtualLine = 0; virtualLine < docLineCount; virtualLine += lineIncrement) { int realLineNumber = m_view->textFolding().visibleLineToLine(virtualLine); QString lineText = m_doc->line(realLineNumber); if (!simpleMode) { m_doc->buffer().ensureHighlighted(realLineNumber); } const Kate::TextLine &kateline = m_doc->plainKateTextLine(realLineNumber); const QVector &attributes = kateline->attributesList(); QVector decorations = m_view->renderer()->decorationsForLine(kateline, realLineNumber); int attributeIndex = 0; // Draw selection if it is on an empty line if (selection.contains(KTextEditor::Cursor(realLineNumber, 0)) && lineText.size() == 0) { if (selectionBgColor != painter.pen().color()) { painter.setPen(selectionBgColor); } painter.drawLine(s_pixelMargin, pixelY, s_pixelMargin + s_lineWidth - 1, pixelY); } // Iterate over the line to draw the background int selStartX = -1; int selEndX = -1; int pixelX = s_pixelMargin; // use this to control the offset of the text from the left for (int x = 0; (x < lineText.size() && x < s_lineWidth); x += charIncrement) { if (pixelX >= s_lineWidth + s_pixelMargin) { break; } // Query the selection and draw it behind the character if (selection.contains(KTextEditor::Cursor(realLineNumber, x))) { if (selStartX == -1) selStartX = pixelX; selEndX = pixelX; if (lineText.size() - 1 == x) { selEndX = s_lineWidth + s_pixelMargin - 1; } } if (lineText[x] == QLatin1Char('\t')) { pixelX += qMax(4 / charIncrement, 1); // FIXME: tab width... } else { pixelX++; } } if (selStartX != -1) { if (selectionBgColor != painter.pen().color()) { painter.setPen(selectionBgColor); } painter.drawLine(selStartX, pixelY, selEndX, pixelY); } // Iterate over all the characters in the current line pixelX = s_pixelMargin; for (int x = 0; (x < lineText.size() && x < s_lineWidth); x += charIncrement) { if (pixelX >= s_lineWidth + s_pixelMargin) { break; } // draw the pixels if (lineText[x] == QLatin1Char(' ')) { pixelX++; } else if (lineText[x] == QLatin1Char('\t')) { pixelX += qMax(4 / charIncrement, 1); // FIXME: tab width... } else { const QColor newPenColor(charColor(attributes, attributeIndex, decorations, defaultTextColor, x, lineText[x])); if (newPenColor != painter.pen().color()) { painter.setPen(newPenColor); } // Actually draw the pixel with the color queried from the renderer. painter.drawPoint(pixelX, pixelY); pixelX++; } } drawnLines++; if (((drawnLines) % charIncrement) == 0) { pixelY++; } } // qCDebug(LOG_KTE) << drawnLines; // Draw line modification marker map. // Disable this if the document is really huge, // since it requires querying every line. if (m_doc->lines() < 50000) { for (int lineno = 0; lineno < docLineCount; lineno++) { int realLineNo = m_view->textFolding().visibleLineToLine(lineno); const Kate::TextLine &line = m_doc->plainKateTextLine(realLineNo); const QColor &col = line->markedAsModified() ? modifiedLineColor : savedLineColor; if (line->markedAsModified() || line->markedAsSavedOnDisk()) { painter.fillRect(2, lineno / lineDivisor, 3, 1, col); } } } // end painting painter.end(); } // set right ratio m_pixmap.setDevicePixelRatio(m_view->devicePixelRatioF()); // qCDebug(LOG_KTE) << time.elapsed(); // Redraw the scrollbar widget with the updated pixmap. update(); } void KateScrollBar::miniMapPaintEvent(QPaintEvent *e) { QScrollBar::paintEvent(e); QPainter painter(this); QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); QRect grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); m_stdGroveRect = grooveRect; if (style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSubLine, this).height() == 0) { int alignMargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &opt, this); grooveRect.moveTop(alignMargin); grooveRect.setHeight(grooveRect.height() - alignMargin); } if (style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarAddLine, this).height() == 0) { int alignMargin = style()->pixelMetric(QStyle::PM_FocusFrameVMargin, &opt, this); grooveRect.setHeight(grooveRect.height() - alignMargin); } m_grooveHeight = grooveRect.height(); const int docXMargin = 1; // style()->drawControl(QStyle::CE_ScrollBarAddLine, &opt, &painter, this); // style()->drawControl(QStyle::CE_ScrollBarSubLine, &opt, &painter, this); // calculate the document size and position const int docHeight = qMin(grooveRect.height(), int(m_pixmap.height() / m_pixmap.devicePixelRatio() * 2)) - 2 * docXMargin; const int yoffset = 1; // top-aligned in stead of center-aligned (grooveRect.height() - docHeight) / 2; const QRect docRect(QPoint(grooveRect.left() + docXMargin, yoffset + grooveRect.top()), QSize(grooveRect.width() - docXMargin, docHeight)); m_mapGroveRect = docRect; // calculate the visible area int max = qMax(maximum() + 1, 1); int visibleStart = value() * docHeight / (max + pageStep()) + docRect.top() + 0.5; int visibleEnd = (value() + pageStep()) * docHeight / (max + pageStep()) + docRect.top(); QRect visibleRect = docRect; visibleRect.moveTop(visibleStart); visibleRect.setHeight(visibleEnd - visibleStart); // calculate colors const QColor backgroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->background().color(); const QColor foregroundColor = m_view->defaultStyleAttribute(KTextEditor::dsNormal)->foreground().color(); const QColor highlightColor = palette().link().color(); const int backgroundLightness = backgroundColor.lightness(); const int foregroundLightness = foregroundColor.lightness(); const int lighnessDiff = (foregroundLightness - backgroundLightness); // get a color suited for the color theme QColor darkShieldColor = palette().color(QPalette::Mid); int hue, sat, light; darkShieldColor.getHsl(&hue, &sat, &light); // apply suitable lightness darkShieldColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.35); // gradient for nicer results QLinearGradient gradient(0, 0, width(), 0); gradient.setColorAt(0, darkShieldColor); gradient.setColorAt(0.3, darkShieldColor.lighter(115)); gradient.setColorAt(1, darkShieldColor); QColor lightShieldColor; lightShieldColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.15); QColor outlineColor; outlineColor.setHsl(hue, sat, backgroundLightness + lighnessDiff * 0.5); // draw the grove background in case the document is small painter.setPen(Qt::NoPen); painter.setBrush(backgroundColor); painter.drawRect(grooveRect); // adjust the rectangles QRect sliderRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this); sliderRect.setX(docXMargin); sliderRect.setWidth(width() - docXMargin * 2); if ((docHeight + 2 * docXMargin >= grooveRect.height()) && (sliderRect.height() > visibleRect.height() + 2)) { visibleRect.adjust(2, 0, -3, 0); } else { visibleRect.adjust(1, 0, -1, 2); sliderRect.setTop(visibleRect.top() - 1); sliderRect.setBottom(visibleRect.bottom() + 1); } // Smooth transform only when squeezing if (grooveRect.height() < m_pixmap.height() / m_pixmap.devicePixelRatio()) { painter.setRenderHint(QPainter::SmoothPixmapTransform); } // draw the modified lines margin QRect pixmapMarginRect(QPoint(0, 0), QSize(s_pixelMargin, m_pixmap.height() / m_pixmap.devicePixelRatio())); QRect docPixmapMarginRect(QPoint(0, docRect.top()), QSize(s_pixelMargin, docRect.height())); painter.drawPixmap(docPixmapMarginRect, m_pixmap, pixmapMarginRect); // calculate the stretch and draw the stretched lines (scrollbar marks) QRect pixmapRect(QPoint(s_pixelMargin, 0), QSize(m_pixmap.width() / m_pixmap.devicePixelRatio() - s_pixelMargin, m_pixmap.height() / m_pixmap.devicePixelRatio())); QRect docPixmapRect(QPoint(s_pixelMargin, docRect.top()), QSize(docRect.width() - s_pixelMargin, docRect.height())); painter.drawPixmap(docPixmapRect, m_pixmap, pixmapRect); // delimit the end of the document const int y = docPixmapRect.height() + grooveRect.y(); if (y + 2 < grooveRect.y() + grooveRect.height()) { QColor fg(foregroundColor); fg.setAlpha(30); painter.setBrush(Qt::NoBrush); painter.setPen(QPen(fg, 1)); painter.drawLine(grooveRect.x() + 1, y + 2, width() - 1, y + 2); } // fade the invisible sections const QRect top(grooveRect.x(), grooveRect.y(), grooveRect.width(), visibleRect.y() - grooveRect.y() // Pen width ); const QRect bottom(grooveRect.x(), grooveRect.y() + visibleRect.y() + visibleRect.height() - grooveRect.y(), // Pen width grooveRect.width(), grooveRect.height() - (visibleRect.y() - grooveRect.y()) - visibleRect.height()); QColor faded(backgroundColor); faded.setAlpha(110); painter.fillRect(top, faded); painter.fillRect(bottom, faded); // add a thin line to limit the scrollbar QColor c(foregroundColor); c.setAlpha(10); painter.setPen(QPen(c, 1)); painter.drawLine(0, 0, 0, height()); if (m_showMarks) { QHashIterator it = m_lines; QPen penBg; penBg.setWidth(4); lightShieldColor.setAlpha(180); penBg.setColor(lightShieldColor); painter.setPen(penBg); while (it.hasNext()) { it.next(); int y = (it.key() - grooveRect.top()) * docHeight / grooveRect.height() + docRect.top(); ; painter.drawLine(6, y, width() - 6, y); } it = m_lines; QPen pen; pen.setWidth(2); while (it.hasNext()) { it.next(); pen.setColor(it.value()); painter.setPen(pen); int y = (it.key() - grooveRect.top()) * docHeight / grooveRect.height() + docRect.top(); painter.drawLine(6, y, width() - 6, y); } } // slider outline QColor sliderColor(highlightColor); sliderColor.setAlpha(50); painter.fillRect(sliderRect, sliderColor); painter.setPen(QPen(highlightColor, 0)); // rounded rect looks ugly for some reason, so we draw 4 lines. painter.drawLine(sliderRect.left(), sliderRect.top() + 1, sliderRect.left(), sliderRect.bottom() - 1); painter.drawLine(sliderRect.right(), sliderRect.top() + 1, sliderRect.right(), sliderRect.bottom() - 1); painter.drawLine(sliderRect.left() + 1, sliderRect.top(), sliderRect.right() - 1, sliderRect.top()); painter.drawLine(sliderRect.left() + 1, sliderRect.bottom(), sliderRect.right() - 1, sliderRect.bottom()); } void KateScrollBar::normalPaintEvent(QPaintEvent *e) { QScrollBar::paintEvent(e); if (!m_showMarks) { return; } QPainter painter(this); QStyleOptionSlider opt; opt.init(this); opt.subControls = QStyle::SC_None; opt.activeSubControls = QStyle::SC_None; opt.orientation = orientation(); opt.minimum = minimum(); opt.maximum = maximum(); opt.sliderPosition = sliderPosition(); opt.sliderValue = value(); opt.singleStep = singleStep(); opt.pageStep = pageStep(); QRect rect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarSlider, this); int sideMargin = width() - rect.width(); if (sideMargin < 4) { sideMargin = 4; } sideMargin /= 2; QHashIterator it = m_lines; while (it.hasNext()) { it.next(); painter.setPen(it.value()); if (it.key() < rect.top() || it.key() > rect.bottom()) { painter.drawLine(0, it.key(), width(), it.key()); } else { painter.drawLine(0, it.key(), sideMargin, it.key()); painter.drawLine(width() - sideMargin, it.key(), width(), it.key()); } } } void KateScrollBar::resizeEvent(QResizeEvent *e) { QScrollBar::resizeEvent(e); m_updateTimer.start(); m_lines.clear(); update(); } void KateScrollBar::sliderChange(SliderChange change) { // call parents implementation QScrollBar::sliderChange(change); if (change == QAbstractSlider::SliderValueChange) { redrawMarks(); } else if (change == QAbstractSlider::SliderRangeChange) { marksChanged(); } if (m_leftMouseDown || m_middleMouseDown) { const int fromLine = m_viewInternal->toRealCursor(m_viewInternal->startPos()).line() + 1; const int lastLine = m_viewInternal->toRealCursor(m_viewInternal->endPos()).line() + 1; QToolTip::showText(m_toolTipPos, i18nc("from line - to line", "
%1

%2
", fromLine, lastLine), this); } } void KateScrollBar::marksChanged() { m_lines.clear(); update(); } void KateScrollBar::redrawMarks() { if (!m_showMarks) { return; } update(); } void KateScrollBar::recomputeMarksPositions() { // get the style options to compute the scrollbar pixels QStyleOptionSlider opt; initStyleOption(&opt); QRect grooveRect = style()->subControlRect(QStyle::CC_ScrollBar, &opt, QStyle::SC_ScrollBarGroove, this); // cache top margin and groove height const int top = grooveRect.top(); const int h = grooveRect.height() - 1; // make sure we have a sane height if (h <= 0) { return; } // get total visible (=without folded) lines in the document int visibleLines = m_view->textFolding().visibleLines() - 1; if (m_view->config()->scrollPastEnd()) { visibleLines += m_viewInternal->linesDisplayed() - 1; visibleLines -= m_view->config()->autoCenterLines(); } // now repopulate the scrollbar lines list m_lines.clear(); const QHash &marks = m_doc->marks(); for (QHash::const_iterator i = marks.constBegin(); i != marks.constEnd(); ++i) { KTextEditor::Mark *mark = i.value(); const int line = m_view->textFolding().lineToVisibleLine(mark->line); const double ratio = static_cast(line) / visibleLines; m_lines.insert(top + (int)(h * ratio), KateRendererConfig::global()->lineMarkerColor((KTextEditor::MarkInterface::MarkTypes)mark->type)); } } void KateScrollBar::sliderMaybeMoved(int value) { if (m_middleMouseDown) { // we only need to emit this signal once, as for the following slider // movements the signal sliderMoved() is already emitted. // Thus, set m_middleMouseDown to false right away. m_middleMouseDown = false; emit sliderMMBMoved(value); } } // END // BEGIN KateCmdLineEditFlagCompletion /** * This class provide completion of flags. It shows a short description of * each flag, and flags are appended. */ class KateCmdLineEditFlagCompletion : public KCompletion { public: KateCmdLineEditFlagCompletion() { ; } QString makeCompletion(const QString & /*s*/) override { return QString(); } }; // END KateCmdLineEditFlagCompletion // BEGIN KateCmdLineEdit KateCommandLineBar::KateCommandLineBar(KTextEditor::ViewPrivate *view, QWidget *parent) : KateViewBarWidget(true, parent) { QHBoxLayout *topLayout = new QHBoxLayout(); centralWidget()->setLayout(topLayout); topLayout->setContentsMargins(0, 0, 0, 0); m_lineEdit = new KateCmdLineEdit(this, view); connect(m_lineEdit, SIGNAL(hideRequested()), SIGNAL(hideMe())); topLayout->addWidget(m_lineEdit); QToolButton *helpButton = new QToolButton(this); helpButton->setAutoRaise(true); helpButton->setIcon(QIcon::fromTheme(QStringLiteral("help-contextual"))); topLayout->addWidget(helpButton); connect(helpButton, SIGNAL(clicked()), this, SLOT(showHelpPage())); setFocusProxy(m_lineEdit); } void KateCommandLineBar::showHelpPage() { KHelpClient::invokeHelp(QStringLiteral("advanced-editing-tools-commandline"), QStringLiteral("kate")); } KateCommandLineBar::~KateCommandLineBar() { } // inserts the given string in the command line edit and (if selected = true) selects it so the user // can type over it if they want to void KateCommandLineBar::setText(const QString &text, bool selected) { m_lineEdit->setText(text); if (selected) { m_lineEdit->selectAll(); } } void KateCommandLineBar::execute(const QString &text) { m_lineEdit->slotReturnPressed(text); } KateCmdLineEdit::KateCmdLineEdit(KateCommandLineBar *bar, KTextEditor::ViewPrivate *view) : KLineEdit() , m_view(view) , m_bar(bar) , m_msgMode(false) , m_histpos(0) , m_cmdend(0) , m_command(nullptr) { connect(this, SIGNAL(returnPressed(QString)), this, SLOT(slotReturnPressed(QString))); setCompletionObject(KateCmd::self()->commandCompletionObject()); setAutoDeleteCompletionObject(false); m_hideTimer = new QTimer(this); m_hideTimer->setSingleShot(true); connect(m_hideTimer, SIGNAL(timeout()), this, SLOT(hideLineEdit())); // make sure the timer is stopped when the user switches views. if not, focus will be given to the // wrong view when KateViewBar::hideCurrentBarWidget() is called after 4 seconds. (the timer is // used for showing things like "Success" for four seconds after the user has used the kate // command line) connect(m_view, SIGNAL(focusOut(KTextEditor::View *)), m_hideTimer, SLOT(stop())); } void KateCmdLineEdit::hideEvent(QHideEvent *e) { Q_UNUSED(e); } QString KateCmdLineEdit::helptext(const QPoint &) const { const QString beg = QStringLiteral("
Help: "); const QString mid = QStringLiteral("
"); const QString end = QStringLiteral("
"); const QString t = text(); static const QRegularExpression re(QStringLiteral("\\s*help\\s+(.*)")); auto match = re.match(t); if (match.hasMatch()) { QString s; // get help for command const QString name = match.captured(1); if (name == QLatin1String("list")) { return beg + i18n("Available Commands") + mid + KateCmd::self()->commandList().join(QLatin1Char(' ')) + i18n("

For help on individual commands, do 'help <command>'

") + end; } else if (!name.isEmpty()) { KTextEditor::Command *cmd = KateCmd::self()->queryCommand(name); if (cmd) { if (cmd->help(m_view, name, s)) { return beg + name + mid + s + end; } else { return beg + name + mid + i18n("No help for '%1'", name) + end; } } else { return beg + mid + i18n("No such command %1", name) + end; } } } return beg + mid + i18n("

This is the Katepart command line.
" "Syntax: command [ arguments ]
" "For a list of available commands, enter help list
" "For help for individual commands, enter help <command>

") + end; } bool KateCmdLineEdit::event(QEvent *e) { if (e->type() == QEvent::QueryWhatsThis) { setWhatsThis(helptext(QPoint())); e->accept(); return true; } return KLineEdit::event(e); } /** * Parse the text as a command. * * The following is a simple PEG grammar for the syntax of the command. * (A PEG grammar is like a BNF grammar, except that "/" stands for * ordered choice: only the first matching rule is used. In other words, * the parsing is short-circuited in the manner of the "or" operator in * programming languages, and so the grammar is unambiguous.) * * Text <- Range? Command * / Position * Range <- Position ("," Position)? * / "%" * Position <- Base Offset? * Base <- Line * / LastLine * / ThisLine * / Mark * Offset <- [+-] Base * Line <- [0-9]+ * LastLine <- "$" * ThisLine <- "." * Mark <- "'" [a-z] */ void KateCmdLineEdit::slotReturnPressed(const QString &text) { static const QRegularExpression focusChangingCommands(QStringLiteral("^(buffer|b|new|vnew|bp|bprev|bn|bnext|bf|bfirst|bl|blast|edit|e)$")); if (text.isEmpty()) { return; } // silently ignore leading space characters uint n = 0; const uint textlen = text.length(); while ((n < textlen) && (text[n].isSpace())) { n++; } if (n >= textlen) { return; } QString cmd = text.mid(n); // Parse any leading range expression, and strip it (and maybe do some other transforms on the command). QString leadingRangeExpression; KTextEditor::Range range = CommandRangeExpressionParser::parseRangeExpression(cmd, m_view, leadingRangeExpression, cmd); // Built in help: if the command starts with "help", [try to] show some help if (cmd.startsWith(QLatin1String("help"))) { QWhatsThis::showText(mapToGlobal(QPoint(0, 0)), helptext(QPoint())); clear(); KateCmd::self()->appendHistory(cmd); m_histpos = KateCmd::self()->historyLength(); m_oldText.clear(); return; } if (cmd.length() > 0) { KTextEditor::Command *p = KateCmd::self()->queryCommand(cmd); m_oldText = leadingRangeExpression + cmd; m_msgMode = true; // if the command changes the focus itself, the bar should be hidden before execution. if (focusChangingCommands.match(cmd.leftRef(cmd.indexOf(QLatin1Char(' ')))).hasMatch()) { emit hideRequested(); } if (!p) { setText(i18n("No such command: \"%1\"", cmd)); } else if (range.isValid() && !p->supportsRange(cmd)) { // Raise message, when the command does not support ranges. setText(i18n("Error: No range allowed for command \"%1\".", cmd)); } else { QString msg; if (p->exec(m_view, cmd, msg, range)) { // append command along with range (will be empty if none given) to history KateCmd::self()->appendHistory(leadingRangeExpression + cmd); m_histpos = KateCmd::self()->historyLength(); m_oldText.clear(); if (msg.length() > 0) { setText(i18n("Success: ") + msg); } else if (isVisible()) { // always hide on success without message emit hideRequested(); } } else { if (msg.length() > 0) { if (msg.contains(QLatin1Char('\n'))) { // multiline error, use widget with more space QWhatsThis::showText(mapToGlobal(QPoint(0, 0)), msg); } else { setText(msg); } } else { setText(i18n("Command \"%1\" failed.", cmd)); } } } } // clean up if (completionObject() != KateCmd::self()->commandCompletionObject()) { KCompletion *c = completionObject(); setCompletionObject(KateCmd::self()->commandCompletionObject()); delete c; } m_command = nullptr; m_cmdend = 0; if (!focusChangingCommands.match(cmd.leftRef(cmd.indexOf(QLatin1Char(' ')))).hasMatch()) { m_view->setFocus(); } if (isVisible()) { m_hideTimer->start(4000); } } void KateCmdLineEdit::hideLineEdit() // unless i have focus ;) { if (!hasFocus()) { emit hideRequested(); } } void KateCmdLineEdit::focusInEvent(QFocusEvent *ev) { if (m_msgMode) { m_msgMode = false; setText(m_oldText); selectAll(); } KLineEdit::focusInEvent(ev); } void KateCmdLineEdit::keyPressEvent(QKeyEvent *ev) { if (ev->key() == Qt::Key_Escape || (ev->key() == Qt::Key_BracketLeft && ev->modifiers() == Qt::ControlModifier)) { m_view->setFocus(); hideLineEdit(); clear(); } else if (ev->key() == Qt::Key_Up) { fromHistory(true); } else if (ev->key() == Qt::Key_Down) { fromHistory(false); } uint cursorpos = cursorPosition(); KLineEdit::keyPressEvent(ev); // during typing, let us see if we have a valid command if (!m_cmdend || cursorpos <= m_cmdend) { QChar c; if (!ev->text().isEmpty()) { c = ev->text().at(0); } if (!m_cmdend && !c.isNull()) { // we have no command, so lets see if we got one if (!c.isLetterOrNumber() && c != QLatin1Char('-') && c != QLatin1Char('_')) { m_command = KateCmd::self()->queryCommand(text().trimmed()); if (m_command) { // qCDebug(LOG_KTE)<<"keypress in commandline: We have a command! "<queryCommand(text().trimmed()); if (m_command) { // qCDebug(LOG_KTE)<<"keypress in commandline: We have a command! "<commandCompletionObject()) { KCompletion *c = completionObject(); setCompletionObject(KateCmd::self()->commandCompletionObject()); delete c; } m_cmdend = 0; } } // if we got a command, check if it wants to do something. if (m_command) { KCompletion *cmpl = m_command->completionObject(m_view, text().left(m_cmdend).trimmed()); if (cmpl) { // We need to prepend the current command name + flag string // when completion is done // qCDebug(LOG_KTE)<<"keypress in commandline: Setting completion object!"; setCompletionObject(cmpl); } } } else if (m_command && !ev->text().isEmpty()) { // check if we should call the commands processText() if (m_command->wantsToProcessText(text().left(m_cmdend).trimmed())) { m_command->processText(m_view, text()); } } } void KateCmdLineEdit::fromHistory(bool up) { if (!KateCmd::self()->historyLength()) { return; } QString s; if (up) { if (m_histpos > 0) { m_histpos--; s = KateCmd::self()->fromHistory(m_histpos); } } else { if (m_histpos < (KateCmd::self()->historyLength() - 1)) { m_histpos++; s = KateCmd::self()->fromHistory(m_histpos); } else { m_histpos = KateCmd::self()->historyLength(); setText(m_oldText); } } if (!s.isEmpty()) { // Select the argument part of the command, so that it is easy to overwrite setText(s); static const QRegularExpression reCmd(QStringLiteral("^[\\w\\-]+(?:[^a-zA-Z0-9_-]|:\\w+)(.*)")); const auto match = reCmd.match(text()); if (match.hasMatch()) { setSelection(text().length() - match.capturedLength(1), match.capturedLength(1)); } } } // END KateCmdLineEdit // BEGIN KateIconBorder using namespace KTextEditor; KateIconBorder::KateIconBorder(KateViewInternal *internalView, QWidget *parent) : QWidget(parent) , m_view(internalView->m_view) , m_doc(internalView->doc()) , m_viewInternal(internalView) , m_iconBorderOn(false) , m_lineNumbersOn(false) , m_relLineNumbersOn(false) , m_updateRelLineNumbers(false) , m_foldingMarkersOn(false) , m_dynWrapIndicatorsOn(false) , m_annotationBorderOn(false) , m_updatePositionToArea(true) , m_annotationItemDelegate(new KateAnnotationItemDelegate(m_viewInternal, this)) { setAcceptDrops(true); setAttribute(Qt::WA_StaticContents); // See: https://doc.qt.io/qt-5/qwidget.html#update. As this widget does not // have a background, there's no need for Qt to erase the widget's area // before repainting. Enabling this prevents flickering when the widget is // repainted. setAttribute(Qt::WA_OpaquePaintEvent); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Minimum); setMouseTracking(true); m_doc->setMarkDescription(MarkInterface::markType01, i18n("Bookmark")); m_doc->setMarkPixmap(MarkInterface::markType01, QIcon::fromTheme(QStringLiteral("bookmarks")).pixmap(32, 32)); connect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); updateFont(); m_antiFlickerTimer.setSingleShot(true); m_antiFlickerTimer.setInterval(300); connect(&m_antiFlickerTimer, &QTimer::timeout, this, &KateIconBorder::highlightFolding); // user interaction (scrolling) hides e.g. preview connect(m_view, SIGNAL(displayRangeChanged(KTextEditor::ViewPrivate *)), this, SLOT(displayRangeChanged())); } KateIconBorder::~KateIconBorder() { delete m_foldingPreview; delete m_foldingRange; } void KateIconBorder::setIconBorderOn(bool enable) { if (enable == m_iconBorderOn) { return; } m_iconBorderOn = enable; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setAnnotationBorderOn(bool enable) { if (enable == m_annotationBorderOn) { return; } m_annotationBorderOn = enable; // make sure the tooltip is hidden if (!m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); hideAnnotationTooltip(); } emit m_view->annotationBorderVisibilityChanged(m_view, enable); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::removeAnnotationHovering() { // remove hovering if it's still there if (m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::setLineNumbersOn(bool enable) { if (enable == m_lineNumbersOn) { return; } m_lineNumbersOn = enable; m_dynWrapIndicatorsOn = (m_dynWrapIndicators == 1) ? enable : m_dynWrapIndicators; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setRelLineNumbersOn(bool enable) { if (enable == m_relLineNumbersOn) { return; } m_relLineNumbersOn = enable; /* * We don't have to touch the m_dynWrapIndicatorsOn because * we already got it right from the m_lineNumbersOn */ m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::updateForCursorLineChange() { if (m_relLineNumbersOn) { m_updateRelLineNumbers = true; } // always do normal update, e.g. for different current line color! update(); } void KateIconBorder::setDynWrapIndicators(int state) { if (state == m_dynWrapIndicators) { return; } m_dynWrapIndicators = state; m_dynWrapIndicatorsOn = (state == 1) ? m_lineNumbersOn : state; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::setFoldingMarkersOn(bool enable) { if (enable == m_foldingMarkersOn) { return; } m_foldingMarkersOn = enable; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } QSize KateIconBorder::sizeHint() const { int w = 1; // Must be any value != 0 or we will never painted! const int i = m_positionToArea.size(); if (i > 0) { w = m_positionToArea.at(i - 1).first; } return QSize(w, 0); } // This function (re)calculates the maximum width of any of the digit characters (0 -> 9) // for graceful handling of variable-width fonts as the linenumber font. void KateIconBorder::updateFont() { const QFontMetricsF &fm = m_view->renderer()->currentFontMetrics(); m_maxCharWidth = 0.0; // Loop to determine the widest numeric character in the current font. // 48 is ascii '0' for (int i = 48; i < 58; i++) { const qreal charWidth = ceil(fm.boundingRect(QChar(i)).width()); m_maxCharWidth = qMax(m_maxCharWidth, charWidth); } // NOTE/TODO(or not) Take size of m_dynWrapIndicatorChar into account. // It's a multi-char and it's reported size is, even with a mono-space font, // bigger than each digit, e.g. 10 vs 12. Currently it seems to work even with // "Line Numbers Off" but all these width calculating looks slightly hacky // the icon pane scales with the font... m_iconAreaWidth = fm.height(); // Only for now, later may that become an own value m_foldingAreaWidth = m_iconAreaWidth; calcAnnotationBorderWidth(); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } int KateIconBorder::lineNumberWidth() const { int width = 0; // Avoid unneeded expensive calculations ;-) if (m_lineNumbersOn) { // width = (number of digits + 1) * char width const int digits = (int)ceil(log10((double)(m_view->doc()->lines() + 1))); width = (int)ceil((digits + 1) * m_maxCharWidth); } if ((width < 1) && m_dynWrapIndicatorsOn && m_view->config()->dynWordWrap()) { // FIXME Why 2x? because of above (number of digits + 1) // -> looks to me like a hint for bad calculation elsewhere width = 2 * m_maxCharWidth; } return width; } void KateIconBorder::dragMoveEvent(QDragMoveEvent *event) { // FIXME Just calling m_view->m_viewInternal->dragMoveEvent(e) don't work // as intended, we need to set the cursor at column 1 // Is there a way to change the pos of the event? QPoint pos(0, event->pos().y()); // Code copy of KateViewInternal::dragMoveEvent m_view->m_viewInternal->placeCursor(pos, true, false); m_view->m_viewInternal->fixDropEvent(event); } void KateIconBorder::dropEvent(QDropEvent *event) { m_view->m_viewInternal->dropEvent(event); } void KateIconBorder::paintEvent(QPaintEvent *e) { paintBorder(e->rect().x(), e->rect().y(), e->rect().width(), e->rect().height()); } static void paintTriangle(QPainter &painter, QColor c, int xOffset, int yOffset, int width, int height, bool open) { painter.setRenderHint(QPainter::Antialiasing); qreal size = qMin(width, height); if (open) { // Paint unfolded icon less pushy if (KColorUtils::luma(c) < 0.25) { c = KColorUtils::darken(c); } else { c = KColorUtils::shade(c, 0.1); } } else { // Paint folded icon in contrast to popup highlighting if (KColorUtils::luma(c) > 0.25) { c = KColorUtils::darken(c); } else { c = KColorUtils::shade(c, 0.1); } } QPen pen; pen.setJoinStyle(Qt::RoundJoin); pen.setColor(c); pen.setWidthF(1.5); painter.setPen(pen); painter.setBrush(c); // let some border, if possible size *= 0.6; qreal halfSize = size / 2; qreal halfSizeP = halfSize * 0.6; QPointF middle(xOffset + (qreal)width / 2, yOffset + (qreal)height / 2); if (open) { QPointF points[3] = {middle + QPointF(-halfSize, -halfSizeP), middle + QPointF(halfSize, -halfSizeP), middle + QPointF(0, halfSizeP)}; painter.drawConvexPolygon(points, 3); } else { QPointF points[3] = {middle + QPointF(-halfSizeP, -halfSize), middle + QPointF(-halfSizeP, halfSize), middle + QPointF(halfSizeP, 0)}; painter.drawConvexPolygon(points, 3); } painter.setRenderHint(QPainter::Antialiasing, false); } /** * Helper class for an identifier which can be an empty or non-empty string or invalid. * Avoids complicated explicit statements in code dealing with the identifier * received as QVariant from a model. */ class KateAnnotationGroupIdentifier { public: KateAnnotationGroupIdentifier(const QVariant &identifier) : m_isValid(identifier.isValid() && identifier.canConvert()) , m_id(m_isValid ? identifier.toString() : QString()) { } KateAnnotationGroupIdentifier() = default; KateAnnotationGroupIdentifier(const KateAnnotationGroupIdentifier &rhs) = default; KateAnnotationGroupIdentifier &operator=(const KateAnnotationGroupIdentifier &rhs) { m_isValid = rhs.m_isValid; m_id = rhs.m_id; return *this; } KateAnnotationGroupIdentifier &operator=(const QVariant &identifier) { m_isValid = (identifier.isValid() && identifier.canConvert()); if (m_isValid) { m_id = identifier.toString(); } else { m_id.clear(); } return *this; } bool operator==(const KateAnnotationGroupIdentifier &rhs) const { return (m_isValid == rhs.m_isValid) && (!m_isValid || (m_id == rhs.m_id)); } bool operator!=(const KateAnnotationGroupIdentifier &rhs) const { return (m_isValid != rhs.m_isValid) || (m_isValid && (m_id != rhs.m_id)); } void clear() { m_isValid = false; m_id.clear(); } bool isValid() const { return m_isValid; } const QString &id() const { return m_id; } private: bool m_isValid = false; QString m_id; }; /** * Helper class for iterative calculation of data regarding the position * of a line with regard to annotation item grouping. */ class KateAnnotationGroupPositionState { public: /** * @param startz first rendered displayed line * @param isUsed flag whether the KateAnnotationGroupPositionState object will * be used or is just created due to being on the stack */ KateAnnotationGroupPositionState(KateViewInternal *viewInternal, const KTextEditor::AnnotationModel *model, const QString &hoveredAnnotationGroupIdentifier, uint startz, bool isUsed); /** * @param styleOption option to fill with data for the given line * @param z rendered displayed line * @param realLine real line which is rendered here (passed to avoid another look-up) */ void nextLine(KTextEditor::StyleOptionAnnotationItem &styleOption, uint z, int realLine); private: KateViewInternal *m_viewInternal; const KTextEditor::AnnotationModel *const m_model; const QString m_hoveredAnnotationGroupIdentifier; int m_visibleWrappedLineInAnnotationGroup = -1; KateAnnotationGroupIdentifier m_lastAnnotationGroupIdentifier; KateAnnotationGroupIdentifier m_nextAnnotationGroupIdentifier; bool m_isSameAnnotationGroupsSinceLast = false; }; KateAnnotationGroupPositionState::KateAnnotationGroupPositionState(KateViewInternal *viewInternal, const KTextEditor::AnnotationModel *model, const QString &hoveredAnnotationGroupIdentifier, uint startz, bool isUsed) : m_viewInternal(viewInternal) , m_model(model) , m_hoveredAnnotationGroupIdentifier(hoveredAnnotationGroupIdentifier) { if (!isUsed) { return; } if (!m_model || (static_cast(startz) >= m_viewInternal->cache()->viewCacheLineCount())) { return; } const auto realLineAtStart = m_viewInternal->cache()->viewLine(startz).line(); m_nextAnnotationGroupIdentifier = m_model->data(realLineAtStart, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (m_nextAnnotationGroupIdentifier.isValid()) { // estimate state of annotation group before first rendered line if (startz == 0) { if (realLineAtStart > 0) { // TODO: here we would want to scan until the next line that would be displayed, // to see if there are any group changes until then // for now simply taking neighbour line into account, not a grave bug on the first displayed line m_lastAnnotationGroupIdentifier = m_model->data(realLineAtStart - 1, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); m_isSameAnnotationGroupsSinceLast = (m_lastAnnotationGroupIdentifier == m_nextAnnotationGroupIdentifier); } } else { const auto realLineBeforeStart = m_viewInternal->cache()->viewLine(startz - 1).line(); m_lastAnnotationGroupIdentifier = m_model->data(realLineBeforeStart, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (m_lastAnnotationGroupIdentifier.isValid()) { if (m_lastAnnotationGroupIdentifier.id() == m_nextAnnotationGroupIdentifier.id()) { m_isSameAnnotationGroupsSinceLast = true; // estimate m_visibleWrappedLineInAnnotationGroup from lines before startz for (uint z = startz; z > 0; --z) { const auto realLine = m_viewInternal->cache()->viewLine(z - 1).line(); const KateAnnotationGroupIdentifier identifier = m_model->data(realLine, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (identifier != m_lastAnnotationGroupIdentifier) { break; } ++m_visibleWrappedLineInAnnotationGroup; } } } } } } void KateAnnotationGroupPositionState::nextLine(KTextEditor::StyleOptionAnnotationItem &styleOption, uint z, int realLine) { styleOption.wrappedLine = m_viewInternal->cache()->viewLine(z).viewLine(); styleOption.wrappedLineCount = m_viewInternal->cache()->viewLineCount(realLine); // Estimate position in group const KateAnnotationGroupIdentifier annotationGroupIdentifier = m_nextAnnotationGroupIdentifier; bool isSameAnnotationGroupsSinceThis = false; // Calculate next line's group identifier // shortcut: assuming wrapped lines are always displayed together, test is simple if (styleOption.wrappedLine + 1 < styleOption.wrappedLineCount) { m_nextAnnotationGroupIdentifier = annotationGroupIdentifier; isSameAnnotationGroupsSinceThis = true; } else { if (static_cast(z + 1) < m_viewInternal->cache()->viewCacheLineCount()) { const int realLineAfter = m_viewInternal->cache()->viewLine(z + 1).line(); // search for any realLine with a different group id, also the non-displayed int rl = realLine + 1; for (; rl <= realLineAfter; ++rl) { m_nextAnnotationGroupIdentifier = m_model->data(rl, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); if (!m_nextAnnotationGroupIdentifier.isValid() || (m_nextAnnotationGroupIdentifier.id() != annotationGroupIdentifier.id())) { break; } } isSameAnnotationGroupsSinceThis = (rl > realLineAfter); if (rl < realLineAfter) { m_nextAnnotationGroupIdentifier = m_model->data(realLineAfter, (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole); } } else { // TODO: check next line after display end m_nextAnnotationGroupIdentifier.clear(); isSameAnnotationGroupsSinceThis = false; } } if (annotationGroupIdentifier.isValid()) { if (m_hoveredAnnotationGroupIdentifier == annotationGroupIdentifier.id()) { styleOption.state |= QStyle::State_MouseOver; } else { styleOption.state &= ~QStyle::State_MouseOver; } if (m_isSameAnnotationGroupsSinceLast) { ++m_visibleWrappedLineInAnnotationGroup; } else { m_visibleWrappedLineInAnnotationGroup = 0; } styleOption.annotationItemGroupingPosition = StyleOptionAnnotationItem::InGroup; if (!m_isSameAnnotationGroupsSinceLast) { styleOption.annotationItemGroupingPosition |= StyleOptionAnnotationItem::GroupBegin; } if (!isSameAnnotationGroupsSinceThis) { styleOption.annotationItemGroupingPosition |= StyleOptionAnnotationItem::GroupEnd; } } else { m_visibleWrappedLineInAnnotationGroup = 0; } styleOption.visibleWrappedLineInGroup = m_visibleWrappedLineInAnnotationGroup; m_lastAnnotationGroupIdentifier = m_nextAnnotationGroupIdentifier; m_isSameAnnotationGroupsSinceLast = isSameAnnotationGroupsSinceThis; } void KateIconBorder::paintBorder(int /*x*/, int y, int /*width*/, int height) { const uint h = m_view->renderer()->lineHeight(); const uint startz = (y / h); const uint endz = qMin(startz + 1 + (height / h), static_cast(m_viewInternal->cache()->viewCacheLineCount())); const uint currentLine = m_view->cursorPosition().line(); // center the folding boxes int m_px = (h - 11) / 2; if (m_px < 0) { m_px = 0; } // Ensure we miss no change of the count of line number digits const int newNeededWidth = lineNumberWidth(); if (m_updatePositionToArea || (newNeededWidth != m_lineNumberAreaWidth)) { m_lineNumberAreaWidth = newNeededWidth; m_updatePositionToArea = true; m_positionToArea.clear(); } QPainter p(this); p.setRenderHints(QPainter::TextAntialiasing); p.setFont(m_view->renderer()->currentFont()); // for line numbers KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); KateAnnotationGroupPositionState annotationGroupPositionState(m_viewInternal, model, m_hoveredAnnotationGroupIdentifier, startz, m_annotationBorderOn); // Fetch often used data only once, improve readability const int w = width(); const QColor iconBarColor = m_view->renderer()->config()->iconBarColor(); // Effective our background const QColor lineNumberColor = m_view->renderer()->config()->lineNumberColor(); const QColor backgroundColor = m_view->renderer()->config()->backgroundColor(); // Of the edit area // Paint the border in chunks line by line for (uint z = startz; z < endz; z++) { // Painting coordinates, lineHeight * lineNumber const uint y = h * z; // Paint the border in chunks left->right, remember used width uint lnX = 0; // Paint background over full width... p.fillRect(lnX, y, w, h, iconBarColor); // ...and overpaint again the end to simulate some margin to the edit area, // so that the text not looks like stuck to the border p.fillRect(w - m_separatorWidth, y, w, h, backgroundColor); const KateTextLayout lineLayout = m_viewInternal->cache()->viewLine(z); int realLine = lineLayout.line(); if (realLine < 0) { // We have reached the end of the document, just paint background continue; } // icon pane if (m_iconBorderOn) { p.setPen(m_view->renderer()->config()->separatorColor()); p.setBrush(m_view->renderer()->config()->separatorColor()); p.drawLine(lnX + m_iconAreaWidth, y, lnX + m_iconAreaWidth, y + h); const uint mrk(m_doc->mark(realLine)); // call only once if (mrk && lineLayout.startCol() == 0) { for (uint bit = 0; bit < 32; bit++) { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes)(1 << bit); if (mrk & markType) { QPixmap px_mark(m_doc->markPixmap(markType)); px_mark.setDevicePixelRatio(devicePixelRatioF()); if (!px_mark.isNull() && h > 0 && m_iconAreaWidth > 0) { // scale up to a usable size const int s = qMin(m_iconAreaWidth * devicePixelRatioF(), h * devicePixelRatioF()) - 2; px_mark = px_mark.scaled(s, s, Qt::KeepAspectRatio, Qt::SmoothTransformation); // center the mark pixmap int x_px = 0.5 * qMax(m_iconAreaWidth - (s / devicePixelRatioF()), 0.0); int y_px = 0.5 * qMax(h - (s / devicePixelRatioF()), 0.0); p.drawPixmap(lnX + x_px, y + y_px, px_mark); } } } } lnX += m_iconAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, IconBorder)); } } // annotation information if (m_annotationBorderOn) { // Draw a border line between annotations and the line numbers p.setPen(lineNumberColor); p.setBrush(lineNumberColor); const qreal borderX = lnX + m_annotationAreaWidth + 0.5; p.drawLine(QPointF(borderX, y + 0.5), QPointF(borderX, y + h - 0.5)); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); styleOption.rect.setRect(lnX, y, m_annotationAreaWidth, h); annotationGroupPositionState.nextLine(styleOption, z, realLine); m_annotationItemDelegate->paint(&p, styleOption, model, realLine); } lnX += m_annotationAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, AnnotationBorder)); } } // line number if (m_lineNumbersOn || m_dynWrapIndicatorsOn) { QColor usedLineNumberColor; const int distanceToCurrent = abs(realLine - static_cast(currentLine)); if (distanceToCurrent == 0) { usedLineNumberColor = m_view->renderer()->config()->currentLineNumberColor(); } else { usedLineNumberColor = lineNumberColor; } p.setPen(usedLineNumberColor); p.setBrush(usedLineNumberColor); if (lineLayout.startCol() == 0) { if (m_relLineNumbersOn) { if (distanceToCurrent == 0) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignLeft | Qt::AlignVCenter, QString::number(realLine + 1)); } else { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, QString::number(distanceToCurrent)); } if (m_updateRelLineNumbers) { m_updateRelLineNumbers = false; update(); } } else if (m_lineNumbersOn) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, QString::number(realLine + 1)); } } else if (m_dynWrapIndicatorsOn) { p.drawText(lnX + m_maxCharWidth / 2, y, m_lineNumberAreaWidth - m_maxCharWidth, h, Qt::TextDontClip | Qt::AlignRight | Qt::AlignVCenter, m_dynWrapIndicatorChar); } lnX += m_lineNumberAreaWidth + m_separatorWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, LineNumbers)); } } // modified line system if (m_view->config()->lineModification() && !m_doc->url().isEmpty()) { const Kate::TextLine tl = m_doc->plainKateTextLine(realLine); if (tl->markedAsModified()) { p.fillRect(lnX, y, m_modAreaWidth, h, m_view->renderer()->config()->modifiedLineColor()); } else if (tl->markedAsSavedOnDisk()) { p.fillRect(lnX, y, m_modAreaWidth, h, m_view->renderer()->config()->savedLineColor()); } else { p.fillRect(lnX, y, m_modAreaWidth, h, iconBarColor); } lnX += m_modAreaWidth; // No m_separatorWidth if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, None)); } } // folding markers if (m_foldingMarkersOn) { const QColor foldingColor(m_view->renderer()->config()->foldingColor()); // possible additional folding highlighting if (m_foldingRange && m_foldingRange->overlapsLine(realLine)) { p.fillRect(lnX, y, m_foldingAreaWidth, h, foldingColor); } if (lineLayout.startCol() == 0) { QVector> startingRanges = m_view->textFolding().foldingRangesStartingOnLine(realLine); bool anyFolded = false; for (int i = 0; i < startingRanges.size(); ++i) { if (startingRanges[i].second & Kate::TextFolding::Folded) { anyFolded = true; } } const Kate::TextLine tl = m_doc->kateTextLine(realLine); if (!startingRanges.isEmpty() || tl->markedAsFoldingStart()) { if (anyFolded) { paintTriangle(p, foldingColor, lnX, y, m_foldingAreaWidth, h, false); } else { // Don't try to use currentLineNumberColor, the folded icon gets also not highligted paintTriangle(p, lineNumberColor, lnX, y, m_foldingAreaWidth, h, true); } } } lnX += m_foldingAreaWidth; if (m_updatePositionToArea) { m_positionToArea.append(AreaPosition(lnX, FoldingMarkers)); } } if (m_updatePositionToArea) { m_updatePositionToArea = false; // Don't forget our "text-stuck-to-border" protector lnX += m_separatorWidth; m_positionToArea.append(AreaPosition(lnX, None)); // Now that we know our needed space, ensure we are painted properly updateGeometry(); update(); return; } } } KateIconBorder::BorderArea KateIconBorder::positionToArea(const QPoint &p) const { for (int i = 0; i < m_positionToArea.size(); ++i) { if (p.x() <= m_positionToArea.at(i).first) { return m_positionToArea.at(i).second; } } return None; } void KateIconBorder::mousePressEvent(QMouseEvent *e) { const KateTextLayout &t = m_viewInternal->yToKateTextLayout(e->y()); if (t.isValid()) { m_lastClickedLine = t.line(); const auto area = positionToArea(e->pos()); // IconBorder and AnnotationBorder have their own behavior; don't forward to view if (area != IconBorder && area != AnnotationBorder) { const auto pos = QPoint(0, e->y()); if (area == LineNumbers && e->button() == Qt::LeftButton && !(e->modifiers() & Qt::ShiftModifier)) { // setup view so the following mousePressEvent will select the line m_viewInternal->beginSelectLine(pos); } QMouseEvent forward(QEvent::MouseButtonPress, pos, e->button(), e->buttons(), e->modifiers()); m_viewInternal->mousePressEvent(&forward); } return e->accept(); } QWidget::mousePressEvent(e); } void KateIconBorder::highlightFoldingDelayed(int line) { if ((line == m_currentLine) || (line >= m_doc->buffer().lines())) { return; } m_currentLine = line; if (m_foldingRange) { // We are for a while in the folding area, no need for delay highlightFolding(); } else if (!m_antiFlickerTimer.isActive()) { m_antiFlickerTimer.start(); } } void KateIconBorder::highlightFolding() { /** * compute to which folding range we belong * FIXME: optimize + perhaps have some better threshold or use timers! */ KTextEditor::Range newRange = KTextEditor::Range::invalid(); for (int line = m_currentLine; line >= qMax(0, m_currentLine - 1024); --line) { /** * try if we have folding range from that line, should be fast per call */ KTextEditor::Range foldingRange = m_doc->buffer().computeFoldingRangeForStartLine(line); if (!foldingRange.isValid()) { continue; } /** * does the range reach us? */ if (foldingRange.overlapsLine(m_currentLine)) { newRange = foldingRange; break; } } if (newRange.isValid() && m_foldingRange && *m_foldingRange == newRange) { // new range equals the old one, nothing to do. return; } // the ranges differ, delete the old, if it exists delete m_foldingRange; m_foldingRange = nullptr; // New range, new preview! delete m_foldingPreview; bool showPreview = false; if (newRange.isValid()) { // When next line is not visible we have a folded range, only then we want a preview! showPreview = !m_view->textFolding().isLineVisible(newRange.start().line() + 1); // qCDebug(LOG_KTE) << "new folding hl-range:" << newRange; m_foldingRange = m_doc->newMovingRange(newRange, KTextEditor::MovingRange::ExpandRight); KTextEditor::Attribute::Ptr attr(new KTextEditor::Attribute()); /** * create highlighting color * we avoid alpha as overpainting leads to ugly lines (https://bugreports.qt.io/browse/QTBUG-66036) */ attr->setBackground(QBrush(m_view->renderer()->config()->foldingColor())); m_foldingRange->setView(m_view); // use z depth defined in moving ranges interface m_foldingRange->setZDepth(-100.0); m_foldingRange->setAttribute(attr); } // show text preview, if a folded region starts here... // ...but only when main window is active (#392396) const bool isWindowActive = !window() || window()->isActiveWindow(); if (showPreview && m_view->config()->foldingPreview() && isWindowActive) { m_foldingPreview = new KateTextPreview(m_view, this); m_foldingPreview->setAttribute(Qt::WA_ShowWithoutActivating); m_foldingPreview->setFrameStyle(QFrame::StyledPanel); // Calc how many lines can be displayed in the popup const int lineHeight = m_view->renderer()->lineHeight(); const int foldingStartLine = m_foldingRange->start().line(); // FIXME Is there really no easier way to find lineInDisplay? const QPoint pos = m_viewInternal->mapFrom(m_view, m_view->cursorToCoordinate(KTextEditor::Cursor(foldingStartLine, 0))); const int lineInDisplay = pos.y() / lineHeight; // Allow slightly overpainting of the view bottom to proper cover all lines const int extra = (m_viewInternal->height() % lineHeight) > (lineHeight * 0.6) ? 1 : 0; const int lineCount = qMin(m_foldingRange->numberOfLines() + 1, m_viewInternal->linesDisplayed() - lineInDisplay + extra); m_foldingPreview->resize(m_viewInternal->width(), lineCount * lineHeight + 2 * m_foldingPreview->frameWidth()); const int xGlobal = mapToGlobal(QPoint(width(), 0)).x(); const int yGlobal = m_view->mapToGlobal(m_view->cursorToCoordinate(KTextEditor::Cursor(foldingStartLine, 0))).y(); m_foldingPreview->move(QPoint(xGlobal, yGlobal) - m_foldingPreview->contentsRect().topLeft()); m_foldingPreview->setLine(foldingStartLine); m_foldingPreview->setCenterView(false); m_foldingPreview->setShowFoldedLines(true); m_foldingPreview->raise(); m_foldingPreview->show(); } } void KateIconBorder::hideFolding() { if (m_antiFlickerTimer.isActive()) { m_antiFlickerTimer.stop(); } m_currentLine = -1; delete m_foldingRange; m_foldingRange = nullptr; delete m_foldingPreview; } void KateIconBorder::leaveEvent(QEvent *event) { hideFolding(); removeAnnotationHovering(); QWidget::leaveEvent(event); } void KateIconBorder::mouseMoveEvent(QMouseEvent *e) { const KateTextLayout &t = m_viewInternal->yToKateTextLayout(e->y()); if (!t.isValid()) { // Cleanup everything which may be shown removeAnnotationHovering(); hideFolding(); } else { const BorderArea area = positionToArea(e->pos()); if (area == FoldingMarkers) { highlightFoldingDelayed(t.line()); } else { hideFolding(); } if (area == AnnotationBorder) { KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { m_hoveredAnnotationGroupIdentifier = model->data(t.line(), (Qt::ItemDataRole)KTextEditor::AnnotationModel::GroupIdentifierRole).toString(); const QPoint viewRelativePos = m_view->mapFromGlobal(e->globalPos()); QHelpEvent helpEvent(QEvent::ToolTip, viewRelativePos, e->globalPos()); KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); styleOption.rect = annotationLineRectInView(t.line()); setStyleOptionLineData(&styleOption, e->y(), t.line(), model, m_hoveredAnnotationGroupIdentifier); m_annotationItemDelegate->helpEvent(&helpEvent, m_view, styleOption, model, t.line()); QTimer::singleShot(0, this, SLOT(update())); } } else { if (area == IconBorder) { m_doc->requestMarkTooltip(t.line(), e->globalPos()); } m_hoveredAnnotationGroupIdentifier.clear(); QTimer::singleShot(0, this, SLOT(update())); } if (area != IconBorder) { QPoint p = m_viewInternal->mapFromGlobal(e->globalPos()); QMouseEvent forward(QEvent::MouseMove, p, e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseMoveEvent(&forward); } } QWidget::mouseMoveEvent(e); } void KateIconBorder::mouseReleaseEvent(QMouseEvent *e) { const int cursorOnLine = m_viewInternal->yToKateTextLayout(e->y()).line(); if (cursorOnLine == m_lastClickedLine && cursorOnLine >= 0 && cursorOnLine <= m_doc->lastLine()) { const BorderArea area = positionToArea(e->pos()); if (area == IconBorder) { if (e->button() == Qt::LeftButton) { if (!m_doc->handleMarkClick(cursorOnLine)) { KateViewConfig *config = m_view->config(); const uint editBits = m_doc->editableMarks(); // is the default or the only editable mark const uint singleMark = qPopulationCount(editBits) > 1 ? editBits & config->defaultMarkType() : editBits; if (singleMark) { if (m_doc->mark(cursorOnLine) & singleMark) { m_doc->removeMark(cursorOnLine, singleMark); } else { m_doc->addMark(cursorOnLine, singleMark); } } else if (config->allowMarkMenu()) { showMarkMenu(cursorOnLine, QCursor::pos()); } } } else if (e->button() == Qt::RightButton) { showMarkMenu(cursorOnLine, QCursor::pos()); } } if (area == FoldingMarkers) { // Prefer the highlighted range over the exact clicked line const int lineToToggle = m_foldingRange ? m_foldingRange->toRange().start().line() : cursorOnLine; if (e->button() == Qt::LeftButton) { m_view->toggleFoldingOfLine(lineToToggle); } else if (e->button() == Qt::RightButton) { m_view->toggleFoldingsInRange(lineToToggle); } delete m_foldingPreview; } if (area == AnnotationBorder) { const bool singleClick = style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this); if (e->button() == Qt::LeftButton && singleClick) { emit m_view->annotationActivated(m_view, cursorOnLine); } else if (e->button() == Qt::RightButton) { showAnnotationMenu(cursorOnLine, e->globalPos()); } } } QMouseEvent forward(QEvent::MouseButtonRelease, QPoint(0, e->y()), e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseReleaseEvent(&forward); } void KateIconBorder::mouseDoubleClickEvent(QMouseEvent *e) { int cursorOnLine = m_viewInternal->yToKateTextLayout(e->y()).line(); if (cursorOnLine == m_lastClickedLine && cursorOnLine <= m_doc->lastLine()) { const BorderArea area = positionToArea(e->pos()); const bool singleClick = style()->styleHint(QStyle::SH_ItemView_ActivateItemOnSingleClick, nullptr, this); if (area == AnnotationBorder && !singleClick) { emit m_view->annotationActivated(m_view, cursorOnLine); } } QMouseEvent forward(QEvent::MouseButtonDblClick, QPoint(0, e->y()), e->button(), e->buttons(), e->modifiers()); m_viewInternal->mouseDoubleClickEvent(&forward); } void KateIconBorder::wheelEvent(QWheelEvent *e) { QCoreApplication::sendEvent(m_viewInternal, e); } void KateIconBorder::showMarkMenu(uint line, const QPoint &pos) { if (m_doc->handleMarkContextMenu(line, pos)) { return; } if (!m_view->config()->allowMarkMenu()) { return; } QMenu markMenu; QMenu selectDefaultMark; auto selectDefaultMarkActionGroup = new QActionGroup(&selectDefaultMark); QVector vec(33); int i = 1; for (uint bit = 0; bit < 32; bit++) { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes)(1 << bit); if (!(m_doc->editableMarks() & markType)) { continue; } QAction *mA; QAction *dMA; const QPixmap icon = m_doc->markPixmap(markType); if (!m_doc->markDescription(markType).isEmpty()) { mA = markMenu.addAction(icon, m_doc->markDescription(markType)); dMA = selectDefaultMark.addAction(icon, m_doc->markDescription(markType)); } else { mA = markMenu.addAction(icon, i18n("Mark Type %1", bit + 1)); dMA = selectDefaultMark.addAction(icon, i18n("Mark Type %1", bit + 1)); } selectDefaultMarkActionGroup->addAction(dMA); mA->setData(i); mA->setCheckable(true); dMA->setData(i + 100); dMA->setCheckable(true); if (m_doc->mark(line) & markType) { mA->setChecked(true); } if (markType & KateViewConfig::global()->defaultMarkType()) { dMA->setChecked(true); } vec[i++] = markType; } if (markMenu.actions().count() == 0) { return; } if (markMenu.actions().count() > 1) { markMenu.addAction(i18n("Set Default Mark Type"))->setMenu(&selectDefaultMark); } QAction *rA = markMenu.exec(pos); if (!rA) { return; } int result = rA->data().toInt(); if (result > 100) { KateViewConfig::global()->setValue(KateViewConfig::DefaultMarkType, vec[result - 100]); } else { MarkInterface::MarkTypes markType = (MarkInterface::MarkTypes)vec[result]; if (m_doc->mark(line) & markType) { m_doc->removeMark(line, markType); } else { m_doc->addMark(line, markType); } } } KTextEditor::AbstractAnnotationItemDelegate *KateIconBorder::annotationItemDelegate() const { return m_annotationItemDelegate; } void KateIconBorder::setAnnotationItemDelegate(KTextEditor::AbstractAnnotationItemDelegate *delegate) { if (delegate == m_annotationItemDelegate) { return; } // reset to default, but already on that? if (!delegate && m_isDefaultAnnotationItemDelegate) { // nothing to do return; } // make sure the tooltip is hidden if (m_annotationBorderOn && !m_hoveredAnnotationGroupIdentifier.isEmpty()) { m_hoveredAnnotationGroupIdentifier.clear(); hideAnnotationTooltip(); } disconnect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); if (!m_isDefaultAnnotationItemDelegate) { disconnect(m_annotationItemDelegate, &QObject::destroyed, this, &KateIconBorder::handleDestroyedAnnotationItemDelegate); } if (!delegate) { // reset to a default delegate m_annotationItemDelegate = new KateAnnotationItemDelegate(m_viewInternal, this); m_isDefaultAnnotationItemDelegate = true; } else { // drop any default delegate if (m_isDefaultAnnotationItemDelegate) { delete m_annotationItemDelegate; m_isDefaultAnnotationItemDelegate = false; } m_annotationItemDelegate = delegate; // catch delegate being destroyed connect(delegate, &QObject::destroyed, this, &KateIconBorder::handleDestroyedAnnotationItemDelegate); } connect(m_annotationItemDelegate, &AbstractAnnotationItemDelegate::sizeHintChanged, this, &KateIconBorder::updateAnnotationBorderWidth); if (m_annotationBorderOn) { updateGeometry(); QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::handleDestroyedAnnotationItemDelegate() { setAnnotationItemDelegate(nullptr); } void KateIconBorder::initStyleOption(KTextEditor::StyleOptionAnnotationItem *styleOption) const { styleOption->initFrom(this); styleOption->view = m_view; styleOption->decorationSize = QSize(m_iconAreaWidth, m_iconAreaWidth); styleOption->contentFontMetrics = m_view->renderer()->currentFontMetrics(); } void KateIconBorder::setStyleOptionLineData(KTextEditor::StyleOptionAnnotationItem *styleOption, int y, int realLine, const KTextEditor::AnnotationModel *model, const QString &annotationGroupIdentifier) const { // calculate rendered displayed line const uint h = m_view->renderer()->lineHeight(); const uint z = (y / h); KateAnnotationGroupPositionState annotationGroupPositionState(m_viewInternal, model, annotationGroupIdentifier, z, true); annotationGroupPositionState.nextLine(*styleOption, z, realLine); } QRect KateIconBorder::annotationLineRectInView(int line) const { int x = 0; if (m_iconBorderOn) { x += m_iconAreaWidth + 2; } const int y = m_view->m_viewInternal->lineToY(line); return QRect(x, y, m_annotationAreaWidth, m_view->renderer()->lineHeight()); } void KateIconBorder::updateAnnotationLine(int line) { // TODO: why has the default value been 8, where is that magic number from? int width = 8; KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); width = m_annotationItemDelegate->sizeHint(styleOption, model, line).width(); } if (width > m_annotationAreaWidth) { m_annotationAreaWidth = width; m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } } void KateIconBorder::showAnnotationMenu(int line, const QPoint &pos) { QMenu menu; QAction a(i18n("Disable Annotation Bar"), &menu); a.setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); menu.addAction(&a); emit m_view->annotationContextMenuAboutToShow(m_view, &menu, line); if (menu.exec(pos) == &a) { m_view->setAnnotationBorderVisible(false); } } void KateIconBorder::hideAnnotationTooltip() { m_annotationItemDelegate->hideTooltip(m_view); } void KateIconBorder::updateAnnotationBorderWidth() { calcAnnotationBorderWidth(); m_updatePositionToArea = true; QTimer::singleShot(0, this, SLOT(update())); } void KateIconBorder::calcAnnotationBorderWidth() { // TODO: another magic number, not matching the one in updateAnnotationLine() m_annotationAreaWidth = 6; KTextEditor::AnnotationModel *model = m_view->annotationModel() ? m_view->annotationModel() : m_doc->annotationModel(); if (model) { KTextEditor::StyleOptionAnnotationItem styleOption; initStyleOption(&styleOption); const int lineCount = m_view->doc()->lines(); if (lineCount > 0) { const int checkedLineCount = m_hasUniformAnnotationItemSizes ? 1 : lineCount; for (int i = 0; i < checkedLineCount; ++i) { const int curwidth = m_annotationItemDelegate->sizeHint(styleOption, model, i).width(); if (curwidth > m_annotationAreaWidth) { m_annotationAreaWidth = curwidth; } } } } } void KateIconBorder::annotationModelChanged(KTextEditor::AnnotationModel *oldmodel, KTextEditor::AnnotationModel *newmodel) { if (oldmodel) { oldmodel->disconnect(this); } if (newmodel) { connect(newmodel, SIGNAL(reset()), this, SLOT(updateAnnotationBorderWidth())); connect(newmodel, SIGNAL(lineChanged(int)), this, SLOT(updateAnnotationLine(int))); } updateAnnotationBorderWidth(); } void KateIconBorder::displayRangeChanged() { hideFolding(); removeAnnotationHovering(); } // END KateIconBorder // BEGIN KateViewEncodingAction -// According to http://www.iana.org/assignments/ianacharset-mib +// According to https://www.iana.org/assignments/ianacharset-mib/ianacharset-mib // the default/unknown mib value is 2. #define MIB_DEFAULT 2 bool lessThanAction(KSelectAction *a, KSelectAction *b) { return a->text() < b->text(); } void KateViewEncodingAction::Private::init() { QList actions; q->setToolBarMode(MenuMode); int i; const auto encodingsByScript = KCharsets::charsets()->encodingsByScript(); for (const QStringList &encodingsForScript : encodingsByScript) { KSelectAction *tmp = new KSelectAction(encodingsForScript.at(0), q); for (i = 1; i < encodingsForScript.size(); ++i) { tmp->addAction(encodingsForScript.at(i)); } q->connect(tmp, SIGNAL(triggered(QAction *)), q, SLOT(_k_subActionTriggered(QAction *))); // tmp->setCheckable(true); actions << tmp; } std::sort(actions.begin(), actions.end(), lessThanAction); for (KSelectAction *action : qAsConst(actions)) { q->addAction(action); } } void KateViewEncodingAction::Private::_k_subActionTriggered(QAction *action) { if (currentSubAction == action) { return; } currentSubAction = action; bool ok = false; int mib = q->mibForName(action->text(), &ok); if (ok) { emit q->KSelectAction::triggered(action->text()); emit q->triggered(q->codecForMib(mib)); } } KateViewEncodingAction::KateViewEncodingAction(KTextEditor::DocumentPrivate *_doc, KTextEditor::ViewPrivate *_view, const QString &text, QObject *parent, bool saveAsMode) : KSelectAction(text, parent) , doc(_doc) , view(_view) , d(new Private(this)) , m_saveAsMode(saveAsMode) { d->init(); connect(menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow())); connect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); } KateViewEncodingAction::~KateViewEncodingAction() { delete d; } void KateViewEncodingAction::slotAboutToShow() { setCurrentCodec(doc->config()->encoding()); } void KateViewEncodingAction::setEncoding(const QString &e) { /** * in save as mode => trigger saveAs */ if (m_saveAsMode) { doc->documentSaveAsWithEncoding(e); return; } /** * else switch encoding */ doc->userSetEncodingForNextReload(); doc->setEncoding(e); view->reloadFile(); } int KateViewEncodingAction::mibForName(const QString &codecName, bool *ok) const { // FIXME logic is good but code is ugly bool success = false; int mib = MIB_DEFAULT; KCharsets *charsets = KCharsets::charsets(); QTextCodec *codec = charsets->codecForName(codecName, success); if (!success) { // Maybe we got a description name instead codec = charsets->codecForName(charsets->encodingForName(codecName), success); } if (codec) { mib = codec->mibEnum(); } if (ok) { *ok = success; } if (success) { return mib; } qCWarning(LOG_KTE) << "Invalid codec name: " << codecName; return MIB_DEFAULT; } QTextCodec *KateViewEncodingAction::codecForMib(int mib) const { if (mib == MIB_DEFAULT) { // FIXME offer to change the default codec return QTextCodec::codecForLocale(); } else { return QTextCodec::codecForMib(mib); } } QTextCodec *KateViewEncodingAction::currentCodec() const { return codecForMib(currentCodecMib()); } bool KateViewEncodingAction::setCurrentCodec(QTextCodec *codec) { disconnect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); int i, j; for (i = 0; i < actions().size(); ++i) { if (actions().at(i)->menu()) { for (j = 0; j < actions().at(i)->menu()->actions().size(); ++j) { if (!j && !actions().at(i)->menu()->actions().at(j)->data().isNull()) { continue; } if (actions().at(i)->menu()->actions().at(j)->isSeparator()) { continue; } if (codec == KCharsets::charsets()->codecForName(actions().at(i)->menu()->actions().at(j)->text())) { d->currentSubAction = actions().at(i)->menu()->actions().at(j); d->currentSubAction->setChecked(true); } else { actions().at(i)->menu()->actions().at(j)->setChecked(false); } } } } connect(this, SIGNAL(triggered(QString)), this, SLOT(setEncoding(QString))); return true; } QString KateViewEncodingAction::currentCodecName() const { return d->currentSubAction->text(); } bool KateViewEncodingAction::setCurrentCodec(const QString &codecName) { return setCurrentCodec(KCharsets::charsets()->codecForName(codecName)); } int KateViewEncodingAction::currentCodecMib() const { return mibForName(currentCodecName()); } bool KateViewEncodingAction::setCurrentCodec(int mib) { return setCurrentCodec(codecForMib(mib)); } // END KateViewEncodingAction // BEGIN KateViewBar related classes KateViewBarWidget::KateViewBarWidget(bool addCloseButton, QWidget *parent) : QWidget(parent) , m_viewBar(nullptr) { QHBoxLayout *layout = new QHBoxLayout(this); // NOTE: Here be cosmetics. layout->setContentsMargins(0, 0, 0, 0); // hide button if (addCloseButton) { QToolButton *hideButton = new QToolButton(this); hideButton->setAutoRaise(true); hideButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); connect(hideButton, SIGNAL(clicked()), SIGNAL(hideMe())); layout->addWidget(hideButton); layout->setAlignment(hideButton, Qt::AlignLeft | Qt::AlignTop); } // widget to be used as parent for the real content m_centralWidget = new QWidget(this); layout->addWidget(m_centralWidget); setLayout(layout); setFocusProxy(m_centralWidget); } KateViewBar::KateViewBar(bool external, QWidget *parent, KTextEditor::ViewPrivate *view) : QWidget(parent) , m_external(external) , m_view(view) , m_permanentBarWidget(nullptr) { m_layout = new QVBoxLayout(this); m_stack = new QStackedWidget(this); m_layout->addWidget(m_stack); m_layout->setContentsMargins(0, 0, 0, 0); m_stack->hide(); hide(); } void KateViewBar::addBarWidget(KateViewBarWidget *newBarWidget) { // just ignore additional adds for already existing widgets if (hasBarWidget(newBarWidget)) { return; } // add new widget, invisible... newBarWidget->hide(); m_stack->addWidget(newBarWidget); newBarWidget->setAssociatedViewBar(this); connect(newBarWidget, SIGNAL(hideMe()), SLOT(hideCurrentBarWidget())); } void KateViewBar::removeBarWidget(KateViewBarWidget *barWidget) { // remove only if there if (!hasBarWidget(barWidget)) { return; } m_stack->removeWidget(barWidget); barWidget->setAssociatedViewBar(nullptr); barWidget->hide(); disconnect(barWidget, nullptr, this, nullptr); } void KateViewBar::addPermanentBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(barWidget); Q_ASSERT(!m_permanentBarWidget); m_stack->addWidget(barWidget); m_stack->setCurrentWidget(barWidget); m_stack->show(); m_permanentBarWidget = barWidget; m_permanentBarWidget->show(); setViewBarVisible(true); } void KateViewBar::removePermanentBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(m_permanentBarWidget == barWidget); Q_UNUSED(barWidget); const bool hideBar = m_stack->currentWidget() == m_permanentBarWidget; m_permanentBarWidget->hide(); m_stack->removeWidget(m_permanentBarWidget); m_permanentBarWidget = nullptr; if (hideBar) { m_stack->hide(); setViewBarVisible(false); } } bool KateViewBar::hasPermanentWidget(KateViewBarWidget *barWidget) const { return (m_permanentBarWidget == barWidget); } void KateViewBar::showBarWidget(KateViewBarWidget *barWidget) { Q_ASSERT(barWidget != nullptr); if (barWidget != qobject_cast(m_stack->currentWidget())) { hideCurrentBarWidget(); } // raise correct widget m_stack->setCurrentWidget(barWidget); barWidget->show(); barWidget->setFocus(Qt::ShortcutFocusReason); m_stack->show(); setViewBarVisible(true); } bool KateViewBar::hasBarWidget(KateViewBarWidget *barWidget) const { return m_stack->indexOf(barWidget) != -1; } void KateViewBar::hideCurrentBarWidget() { KateViewBarWidget *current = qobject_cast(m_stack->currentWidget()); if (current) { current->closed(); } // if we have any permanent widget, make it visible again if (m_permanentBarWidget) { m_stack->setCurrentWidget(m_permanentBarWidget); } else { // else: hide the bar m_stack->hide(); setViewBarVisible(false); } m_view->setFocus(); } void KateViewBar::setViewBarVisible(bool visible) { if (m_external) { if (visible) { m_view->mainWindow()->showViewBar(m_view); } else { m_view->mainWindow()->hideViewBar(m_view); } } else { setVisible(visible); } } bool KateViewBar::hiddenOrPermanent() const { KateViewBarWidget *current = qobject_cast(m_stack->currentWidget()); if (!isVisible() || (m_permanentBarWidget && m_permanentBarWidget == current)) { return true; } return false; } void KateViewBar::keyPressEvent(QKeyEvent *event) { if (event->key() == Qt::Key_Escape) { hideCurrentBarWidget(); return; } QWidget::keyPressEvent(event); } void KateViewBar::hideEvent(QHideEvent *event) { Q_UNUSED(event); // if (!event->spontaneous()) // m_view->setFocus(); } // END KateViewBar related classes KatePasteMenu::KatePasteMenu(const QString &text, KTextEditor::ViewPrivate *view) : KActionMenu(text, view) , m_view(view) { connect(menu(), SIGNAL(aboutToShow()), this, SLOT(slotAboutToShow())); } void KatePasteMenu::slotAboutToShow() { menu()->clear(); /** * insert complete paste history */ int i = 0; for (const QString &text : KTextEditor::EditorPrivate::self()->clipboardHistory()) { /** * get text for the menu ;) */ QString leftPart = (text.size() > 48) ? (text.left(48) + QLatin1String("...")) : text; QAction *a = menu()->addAction(leftPart.replace(QLatin1Char('\n'), QLatin1Char(' ')), this, SLOT(paste())); a->setData(i++); } } void KatePasteMenu::paste() { if (!sender()) { return; } QAction *action = qobject_cast(sender()); if (!action) { return; } // get index int i = action->data().toInt(); if (i >= KTextEditor::EditorPrivate::self()->clipboardHistory().size()) { return; } // paste m_view->paste(&KTextEditor::EditorPrivate::self()->clipboardHistory()[i]); } diff --git a/src/vimode/appcommands.cpp b/src/vimode/appcommands.cpp index 62a7a2f0..b237d113 100644 --- a/src/vimode/appcommands.cpp +++ b/src/vimode/appcommands.cpp @@ -1,551 +1,551 @@ /* SPDX-License-Identifier: LGPL-2.0-or-later Copyright (C) 2009 Erlend Hamberg Copyright (C) 2011 Svyatoslav Kuzmich This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include using namespace KateVi; // BEGIN AppCommands AppCommands *AppCommands::m_instance = nullptr; AppCommands::AppCommands() : KTextEditor::Command({QStringLiteral("q"), QStringLiteral("qa"), QStringLiteral("qall"), QStringLiteral("q!"), QStringLiteral("qa!"), QStringLiteral("qall!"), QStringLiteral("w"), QStringLiteral("wq"), QStringLiteral("wa"), QStringLiteral("wqa"), QStringLiteral("x"), QStringLiteral("xa"), QStringLiteral("new"), QStringLiteral("vnew"), QStringLiteral("e"), QStringLiteral("edit"), QStringLiteral("enew"), QStringLiteral("sp"), QStringLiteral("split"), QStringLiteral("vs"), QStringLiteral("vsplit"), QStringLiteral("only"), QStringLiteral("tabe"), QStringLiteral("tabedit"), QStringLiteral("tabnew"), QStringLiteral("bd"), QStringLiteral("bdelete"), QStringLiteral("tabc"), QStringLiteral("tabclose"), QStringLiteral("clo"), QStringLiteral("close")}) , re_write(QStringLiteral("^w(a)?$")) , re_close(QStringLiteral("^bd(elete)?|tabc(lose)?$")) , re_quit(QStringLiteral("^(w)?q(a|all)?(!)?$")) , re_exit(QStringLiteral("^x(a)?$")) , re_edit(QStringLiteral("^e(dit)?|tabe(dit)?|tabnew$")) , re_tabedit(QStringLiteral("^tabe(dit)?|tabnew$")) , re_new(QStringLiteral("^(v)?new$")) , re_split(QStringLiteral("^sp(lit)?$")) , re_vsplit(QStringLiteral("^vs(plit)?$")) , re_vclose(QStringLiteral("^clo(se)?$")) , re_only(QStringLiteral("^on(ly)?$")) { } AppCommands::~AppCommands() { m_instance = nullptr; } bool AppCommands::exec(KTextEditor::View *view, const QString &cmd, QString &msg, const KTextEditor::Range &) { QStringList args(cmd.split(QRegularExpression(QStringLiteral("\\s+")), QString::SkipEmptyParts)); QString command(args.takeFirst()); KTextEditor::MainWindow *mainWin = view->mainWindow(); KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); QRegularExpressionMatch match; if ((match = re_write.match(command)).hasMatch()) { // TODO: handle writing to specific file if (!match.captured(1).isEmpty()) { // [a]ll const auto docs = app->documents(); for (KTextEditor::Document *doc : docs) { doc->save(); } msg = i18n("All documents written to disk"); } else { view->document()->documentSave(); msg = i18n("Document written to disk"); } } // Other buffer commands are implemented by the KateFileTree plugin else if ((match = re_close.match(command)).hasMatch()) { QTimer::singleShot(0, [app, view]() { app->closeDocument(view->document()); }); } else if ((match = re_quit.match(command)).hasMatch()) { const bool save = !match.captured(1).isEmpty(); // :[w]q const bool allDocuments = !match.captured(2).isEmpty(); // :q[all] const bool doNotPromptForSave = !match.captured(3).isEmpty(); // :q[!] if (allDocuments) { if (save) { const auto docs = app->documents(); for (KTextEditor::Document *doc : docs) { doc->save(); } } if (doNotPromptForSave) { const auto docs = app->documents(); for (KTextEditor::Document *doc : docs) { if (doc->isModified()) { doc->setModified(false); } } } QTimer::singleShot(0, this, SLOT(quit())); } else { if (save && view->document()->isModified()) { view->document()->documentSave(); } if (doNotPromptForSave) { view->document()->setModified(false); } if (mainWin->views().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentView())); } else { if (app->documents().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentDocument())); } else { QTimer::singleShot(0, this, SLOT(quit())); } } } } else if ((match = re_exit.match(command)).hasMatch()) { if (!match.captured(1).isEmpty()) { // a[ll] const auto docs = app->documents(); for (KTextEditor::Document *doc : docs) { doc->save(); } QTimer::singleShot(0, this, SLOT(quit())); } else { if (view->document()->isModified()) { view->document()->documentSave(); } if (app->documents().size() > 1) { QTimer::singleShot(0, this, SLOT(closeCurrentDocument())); } else { QTimer::singleShot(0, this, SLOT(quit())); } } } else if ((match = re_edit.match(command)).hasMatch()) { QString argument = args.join(QLatin1Char(' ')); if (argument.isEmpty() || argument == QLatin1String("!")) { if ((match = re_tabedit.match(command)).hasMatch()) { if (auto doc = app->openUrl(QUrl())) { QTimer::singleShot(0, [mainWin, doc]() { mainWin->activateView(doc); }); } } else { view->document()->documentReload(); } } else { QUrl base = view->document()->url(); QUrl url; QUrl arg2path(argument); if (base.isValid()) { // first try to use the same path as the current open document has url = QUrl(base.resolved(arg2path)); // resolved handles the case where the args is a relative path, and is the same as using QUrl(args) elsewise } else { // else use the cwd - url = QUrl(QUrl(QDir::currentPath() + QLatin1Char('/')).resolved(arg2path)); // + "/" is needed because of http://lists.qt.nokia.com/public/qt-interest/2011-May/033913.html + url = QUrl(QUrl(QDir::currentPath() + QLatin1Char('/')).resolved(arg2path)); // + "/" is needed because of https://lists.qt-project.org/pipermail/qt-interest-old/2011-May/033913.html } QFileInfo file(url.toLocalFile()); KTextEditor::Document *doc = app->findUrl(url); if (!doc) { if (file.exists()) { doc = app->openUrl(url); } else { if ((doc = app->openUrl(QUrl()))) { doc->saveAs(url); } } } if (doc) { QTimer::singleShot(0, [mainWin, doc]() { mainWin->activateView(doc); }); } } // splitView() orientations are reversed from the usual editor convention. // 'vsplit' and 'vnew' use Qt::Horizontal to match vi and the Kate UI actions. } else if ((match = re_new.match(command)).hasMatch()) { if (match.captured(1) == QLatin1String("v")) { // vertical split mainWin->splitView(Qt::Horizontal); } else { // horizontal split mainWin->splitView(Qt::Vertical); } mainWin->openUrl(QUrl()); } else if (command == QLatin1String("enew")) { mainWin->openUrl(QUrl()); } else if ((match = re_split.match(command)).hasMatch()) { mainWin->splitView(Qt::Vertical); // see above } else if ((match = re_vsplit.match(command)).hasMatch()) { mainWin->splitView(Qt::Horizontal); } else if ((match = re_vclose.match(command)).hasMatch()) { QTimer::singleShot(0, this, SLOT(closeCurrentSplitView())); } else if ((match = re_only.match(command)).hasMatch()) { QTimer::singleShot(0, this, SLOT(closeOtherSplitViews())); } return true; } bool AppCommands::help(KTextEditor::View *view, const QString &cmd, QString &msg) { Q_UNUSED(view); if (re_write.match(cmd).hasMatch()) { msg = i18n( "

w/wa — write document(s) to disk

" "

Usage: w[a]

" "

Writes the current document(s) to disk. " "It can be called in two ways:
" " w — writes the current document to disk
" " wa — writes all documents to disk.

" "

If no file name is associated with the document, " "a file dialog will be shown.

"); return true; } else if (re_quit.match(cmd).hasMatch()) { msg = i18n( "

q/qa/wq/wqa — [write and] quit

" "

Usage: [w]q[a]

" "

Quits the application. If w is prepended, it also writes" " the document(s) to disk. This command " "can be called in several ways:
" " q — closes the current view.
" " qa — closes all views, effectively quitting the application.
" " wq — writes the current document to disk and closes its view.
" " wqa — writes all documents to disk and quits.

" "

In all cases, if the view being closed is the last view, the application quits. " "If no file name is associated with the document and it should be written to disk, " "a file dialog will be shown.

"); return true; } else if (re_exit.match(cmd).hasMatch()) { msg = i18n( "

x/xa — write and quit

" "

Usage: x[a]

" "

Saves document(s) and quits (exits). This command " "can be called in two ways:
" " x — closes the current view.
" " xa — closes all views, effectively quitting the application.

" "

In all cases, if the view being closed is the last view, the application quits. " "If no file name is associated with the document and it should be written to disk, " "a file dialog will be shown.

" "

Unlike the 'w' commands, this command only writes the document if it is modified." "

"); return true; } else if (re_split.match(cmd).hasMatch()) { msg = i18n( "

sp,split— Split horizontally the current view into two

" "

Usage: sp[lit]

" "

The result is two views on the same document.

"); return true; } else if (re_vsplit.match(cmd).hasMatch()) { msg = i18n( "

vs,vsplit— Split vertically the current view into two

" "

Usage: vs[plit]

" "

The result is two views on the same document.

"); return true; } else if (re_vclose.match(cmd).hasMatch()) { msg = i18n( "

clo[se]— Close the current view

" "

Usage: clo[se]

" "

After executing it, the current view will be closed.

"); return true; } else if (re_new.match(cmd).hasMatch()) { msg = i18n( "

[v]new — split view and create new document

" "

Usage: [v]new

" "

Splits the current view and opens a new document in the new view." " This command can be called in two ways:
" " new — splits the view horizontally and opens a new document.
" " vnew — splits the view vertically and opens a new document.
" "

"); return true; } else if (re_edit.match(cmd).hasMatch()) { msg = i18n( "

e[dit] — reload current document

" "

Usage: e[dit]

" "

Starts editing the current document again. This is useful to re-edit" " the current file, when it has been changed by another program.

"); return true; } return false; } KTextEditor::View *AppCommands::findViewInDifferentSplitView(KTextEditor::MainWindow *window, KTextEditor::View *view) { const auto views = window->views(); for (KTextEditor::View *it : views) { if (!window->viewsInSameSplitView(it, view)) { return it; } } return nullptr; } void AppCommands::closeCurrentDocument() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::Document *doc = app->activeMainWindow()->activeView()->document(); QTimer::singleShot(0, [app, doc]() { app->closeDocument(doc); }); } void AppCommands::closeCurrentView() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); mw->closeView(mw->activeView()); } void AppCommands::closeCurrentSplitView() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); mw->closeSplitView(mw->activeView()); } void AppCommands::closeOtherSplitViews() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); KTextEditor::MainWindow *mw = app->activeMainWindow(); KTextEditor::View *view = mw->activeView(); KTextEditor::View *viewToRemove = nullptr; while ((viewToRemove = findViewInDifferentSplitView(mw, view))) { mw->closeSplitView(viewToRemove); } } void AppCommands::quit() { KTextEditor::Editor::instance()->application()->quit(); } // END AppCommands // BEGIN KateViBufferCommand BufferCommands *BufferCommands::m_instance = nullptr; BufferCommands::BufferCommands() : KTextEditor::Command({QStringLiteral("ls"), QStringLiteral("b"), QStringLiteral("buffer"), QStringLiteral("bn"), QStringLiteral("bnext"), QStringLiteral("bp"), QStringLiteral("bprevious"), QStringLiteral("tabn"), QStringLiteral("tabnext"), QStringLiteral("tabp"), QStringLiteral("tabprevious"), QStringLiteral("bf"), QStringLiteral("bfirst"), QStringLiteral("bl"), QStringLiteral("blast"), QStringLiteral("tabf"), QStringLiteral("tabfirst"), QStringLiteral("tabl"), QStringLiteral("tablast")}) { } BufferCommands::~BufferCommands() { m_instance = nullptr; } bool BufferCommands::exec(KTextEditor::View *view, const QString &cmd, QString &, const KTextEditor::Range &) { // create list of args QStringList args(cmd.split(QLatin1Char(' '), QString::KeepEmptyParts)); QString command = args.takeFirst(); // same as cmd if split failed QString argument = args.join(QLatin1Char(' ')); if (command == QLatin1String("ls")) { // TODO: open quickview } else if (command == QLatin1String("b") || command == QLatin1String("buffer")) { switchDocument(view, argument); } else if (command == QLatin1String("bp") || command == QLatin1String("bprevious")) { prevBuffer(view); } else if (command == QLatin1String("bn") || command == QLatin1String("bnext")) { nextBuffer(view); } else if (command == QLatin1String("bf") || command == QLatin1String("bfirst")) { firstBuffer(view); } else if (command == QLatin1String("bl") || command == QLatin1String("blast")) { lastBuffer(view); } else if (command == QLatin1String("tabn") || command == QLatin1String("tabnext")) { nextTab(view); } else if (command == QLatin1String("tabp") || command == QLatin1String("tabprevious")) { prevTab(view); } else if (command == QLatin1String("tabf") || command == QLatin1String("tabfirst")) { firstTab(view); } else if (command == QLatin1String("tabl") || command == QLatin1String("tablast")) { lastTab(view); } return true; } void BufferCommands::switchDocument(KTextEditor::View *view, const QString &address) { if (address.isEmpty()) { // no argument: switch to the previous document prevBuffer(view); return; } const int idx = address.toInt(); QList docs = documents(); if (idx > 0 && idx <= docs.size()) { // numerical argument: switch to the nth document activateDocument(view, docs.at(idx - 1)); } else { // string argument: switch to the given file KTextEditor::Document *doc = nullptr; for (KTextEditor::Document *it : docs) { if (it->documentName() == address) { doc = it; break; } } if (doc) { activateDocument(view, doc); } } } void BufferCommands::prevBuffer(KTextEditor::View *view) { const QList docs = documents(); const int idx = docs.indexOf(view->document()); if (idx > 0) { activateDocument(view, docs.at(idx - 1)); } else if (!docs.isEmpty()) { // wrap activateDocument(view, docs.last()); } } void BufferCommands::nextBuffer(KTextEditor::View *view) { QList docs = documents(); const int idx = docs.indexOf(view->document()); if (idx + 1 < docs.size()) { activateDocument(view, docs.at(idx + 1)); } else if (!docs.isEmpty()) { // wrap activateDocument(view, docs.first()); } } void BufferCommands::firstBuffer(KTextEditor::View *view) { auto docs = documents(); if (!docs.isEmpty()) { activateDocument(view, documents().at(0)); } } void BufferCommands::lastBuffer(KTextEditor::View *view) { auto docs = documents(); if (!docs.isEmpty()) { activateDocument(view, documents().last()); } } void BufferCommands::prevTab(KTextEditor::View *view) { prevBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::nextTab(KTextEditor::View *view) { nextBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::firstTab(KTextEditor::View *view) { firstBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::lastTab(KTextEditor::View *view) { lastBuffer(view); // TODO: implement properly, when interface is added } void BufferCommands::activateDocument(KTextEditor::View *view, KTextEditor::Document *doc) { KTextEditor::MainWindow *mainWindow = view->mainWindow(); QTimer::singleShot(0, [mainWindow, doc]() { mainWindow->activateView(doc); }); } QList BufferCommands::documents() { KTextEditor::Application *app = KTextEditor::Editor::instance()->application(); return app->documents(); } bool BufferCommands::help(KTextEditor::View * /*view*/, const QString &cmd, QString &msg) { if (cmd == QLatin1String("b") || cmd == QLatin1String("buffer")) { msg = i18n( "

b,buffer — Edit document N from the document list

" "

Usage: b[uffer] [N]

"); return true; } else if (cmd == QLatin1String("bp") || cmd == QLatin1String("bprevious") || cmd == QLatin1String("tabp") || cmd == QLatin1String("tabprevious")) { msg = i18n( "

bp,bprev — previous buffer

" "

Usage: bp[revious] [N]

" "

Goes to [N]th previous document (\"buffer\") in document list.

" "

[N] defaults to one.

" "

Wraps around the start of the document list.

"); return true; } else if (cmd == QLatin1String("bn") || cmd == QLatin1String("bnext") || cmd == QLatin1String("tabn") || cmd == QLatin1String("tabnext")) { msg = i18n( "

bn,bnext — switch to next document

" "

Usage: bn[ext] [N]

" "

Goes to [N]th next document (\"buffer\") in document list." "[N] defaults to one.

" "

Wraps around the end of the document list.

"); return true; } else if (cmd == QLatin1String("bf") || cmd == QLatin1String("bfirst") || cmd == QLatin1String("tabf") || cmd == QLatin1String("tabfirst")) { msg = i18n( "

bf,bfirst — first document

" "

Usage: bf[irst]

" "

Goes to the first document (\"buffer\") in document list.

"); return true; } else if (cmd == QLatin1String("bl") || cmd == QLatin1String("blast") || cmd == QLatin1String("tabl") || cmd == QLatin1String("tablast")) { msg = i18n( "

bl,blast — last document

" "

Usage: bl[ast]

" "

Goes to the last document (\"buffer\") in document list.

"); return true; } else if (cmd == QLatin1String("ls")) { msg = i18n( "

ls

" "

list current buffers

"); } return false; } // END KateViBufferCommand