diff --git a/tools/chatgui/src/chatview.h b/tools/chatgui/src/chatview.h index 1a3fe7138..6882d1ea1 100644 --- a/tools/chatgui/src/chatview.h +++ b/tools/chatgui/src/chatview.h @@ -15,7 +15,7 @@ struct ChatTextSelection { QPersistentModelIndex index; // Строка начала drag, для которой delegate вычисляет диапазон текста. QPoint clickPos; // Точка press относительно visualRect(index).topLeft(). QPoint dragPos; // Текущая/последняя точка drag в той же системе координат. - int startChar = -1; // UTF-16 начало [startChar,endChar); -1 — delegate ещё не вычислил границы. + int startChar = -1; // UTF-16 начало в видимом тексте [startChar,endChar); -1 — delegate ещё не вычислил границы. int endChar = -1; // UTF-16 конец без включения; недопустимый/пустой диапазон при Copy даёт весь текст строки. bool active = false; // Есть drag-выделение, в том числе после release; не равно isSelecting(). }; @@ -64,7 +64,7 @@ protected: private: /* Прокрутить историю по клавише без модификаторов; true, если событие принято. Фокус сохраняется. */ bool scrollWithKey(QKeyEvent *event); - /* Одна строка: выбранный UTF-16 фрагмент либо весь MsgTextRole; несколько — [time] author: text по возрастанию row. + /* Одна строка: выбранный UTF-16 фрагмент либо весь видимый текст; несколько — [time] author: text по возрастанию row. * Пишет только QClipboard::Clipboard, без selection clipboard; без собственного выделения ничего не меняет. */ void copySelection(); void updateScrollDownBtn(); // Видимость по historical/atBottom; выше низа позиционирует кнопку справа внизу viewport. diff --git a/tools/chatgui/src/message_text.cpp b/tools/chatgui/src/message_text.cpp index ac4248735..05e6349a5 100644 --- a/tools/chatgui/src/message_text.cpp +++ b/tools/chatgui/src/message_text.cpp @@ -1,6 +1,8 @@ #include "message_text.h" #include "messagedelegate.h" #include "debug_ui.h" +#include +#include #include #include #include @@ -13,11 +15,11 @@ #include bool MessageText::isCode(int position) const { - for (const auto& block : code) { - if (position < block.start) break; - if (position <= block.start + block.length) return true; - } - return false; + auto next = std::upper_bound(code.cbegin(), code.cend(), position, + [](int offset, const MessageCodeBlock& block) { return offset < block.start; }); + if (next == code.cbegin()) return false; + const auto& block = *--next; + return position <= block.start + block.length; } namespace { @@ -94,7 +96,7 @@ public: for (offset = block.start; offset < end;) { int newline = message.text.indexOf('\n', offset); if (newline < 0 || newline > end) newline = end; - state = highlightLine(QStringView(message.text).mid(offset, newline - offset), state); + state = highlightLine(message.text.mid(offset, newline - offset), state); offset = newline + 1; } } @@ -114,7 +116,8 @@ protected: MessageText parseMessageText(const QString& source) { MessageText result; - const auto lines = source.split('\n'); + QString normalized = source; normalized.replace("\r\n", "\n"); + const auto lines = normalized.split('\n'); static const QRegularExpression fence(QStringLiteral("^```([A-Za-z0-9_+#.-]{0,32})$")); QVector closing(lines.size(), -1); int next = -1; diff --git a/tools/chatgui/tests/test_invite_links.cpp b/tools/chatgui/tests/test_invite_links.cpp index 9a9a82b87..7d21de30b 100644 --- a/tools/chatgui/tests/test_invite_links.cpp +++ b/tools/chatgui/tests/test_invite_links.cpp @@ -30,6 +30,7 @@ private slots: } void formattedMessageSelection() { QFETCH(bool, outgoing); QFETCH(int, width); QFETCH(bool, dark); + QScopedValueRollback logging(g_debug_config.category_levels[DEBUG_CATEGORY_GENERAL], DEBUG_LEVEL_DEBUG); const QString source = "**Bold** *italic* ~~gone~~\n```cpp\nint x = 42;\n// https://example.org\n```\nhttps://example.com"; const QString visible = "Bold italic gone\nint x = 42;\n// https://example.org\nhttps://example.com"; QStandardItemModel model; @@ -56,16 +57,17 @@ private slots: QImage image(view.viewport()->size(), QImage::Format_ARGB32); image.fill(Qt::white); { QPainter painter(&image); delegate.paint(&painter, option, index); } // Начало берём с реально нарисованной первой строки, конец — с последней кликабельной ссылки. - QPoint start, end; int firstY = -1; - for (int y = option.rect.top(); y <= option.rect.bottom() && firstY < 0; ++y) { + QPoint end; QRect firstLine; + for (int y = option.rect.top(); y <= option.rect.bottom(); ++y) { + QRect ink; for (int x = 0; x < image.width(); ++x) { const auto color = image.pixelColor(x, y); - if (color.red() > 150 && color.blue() > 150 && color.green() < 60) { - firstY = y; start = QPoint(x - 3, y + 2); break; - } + if (color.red() > 150 && color.blue() > 150 && color.green() < 60) ink |= QRect(x, y, 1, 1); } + if (ink.isEmpty() && !firstLine.isEmpty()) break; + firstLine |= ink; } - QVERIFY(firstY >= 0); + QVERIFY(!firstLine.isEmpty()); const QPoint start(firstLine.left() - 3, firstLine.center().y()); QSet links; for (int y = option.rect.top(); y <= option.rect.bottom(); y += 5) { for (int x = 5; x < image.width(); x += 12) { @@ -87,7 +89,7 @@ private slots: QCOMPARE(QApplication::clipboard()->text(), visible); QCOMPARE(activated.count(), 0); QCOMPARE(index.data(MsgTextRole).toString(), source); } - if (outgoing && width == 700 && dark) image.save("/tmp/chat-markup-selection.png"); + if (outgoing && width == 700 && dark) image.save(QDir::tempPath() + "/chat-markup-selection.png"); } void multilineSelectionFollowsPaintedText_data() { diff --git a/tools/chatgui/tests/test_message_text.cpp b/tools/chatgui/tests/test_message_text.cpp index a769027bf..8204871a0 100644 --- a/tools/chatgui/tests/test_message_text.cpp +++ b/tools/chatgui/tests/test_message_text.cpp @@ -24,6 +24,7 @@ private slots: QTest::newRow("code") << "before\n```cpp\n int x;\n\t/* **literal** */\n\n```\nafter" << "before\n int x;\n\t/* **literal** */\n\nafter" << 0 << 1; QTest::newRow("empty-code") << "```\n```" << "" << 0 << 1; + QTest::newRow("windows-newlines") << "```cpp\r\nint x;\r\n```" << "int x;" << 0 << 1; QTest::newRow("blank-code") << "```\n\n\n```" << "\n" << 0 << 1; QTest::newRow("two-blocks") << "```c\nint x;\n```\n**bold**\n```python\npass\n```" << "int x;\nbold\npass" << 1 << 2; @@ -74,6 +75,11 @@ private slots: const auto unknown = formattedMessageText(index); QVERIFY(unknown); QVERIFY(unknown->lightSyntax.isEmpty()); model.setData(index, "```\nint x;\n```", MsgTextRole); const auto plainCode = formattedMessageText(index); QVERIFY(plainCode); QVERIFY(plainCode->lightSyntax.isEmpty()); + model.setData(index, "```\n```", MsgTextRole); + const auto emptyCode = formattedMessageText(index); QVERIFY(emptyCode); + QTextLayout emptyLayout; emptyLayout.setFont(QApplication::font()); + QVERIFY(layoutFormattedMessage(emptyLayout, *emptyCode, 200, false) > 0); + QCOMPARE(emptyLayout.lineCount(), 1); QVERIFY(emptyCode->isCode(emptyLayout.lineAt(0).textStart())); model.setData(index, "**invitation**", MsgTextRole); model.setData(index, "utun://abc", MsgInviteLinkRole); QVERIFY(!formattedMessageText(index)); model.setData(index, QString(), MsgInviteLinkRole); model.setData(index, "poll", MsgContentTypeRole);