[network/ruqola] src/widgets: Fix bug for selecting text (fixed selected text bottom to top which has

Laurent Montel <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 5c6747803e2770687e3ab4ff422fcb01952b85ae by Laurent Montel.
Committed on 26/07/2026 at 21:33.
Pushed by mlaurent into branch 'master'.

Fix bug for selecting text (fixed selected text bottom to top which has

some attachment url)

M  +35   -0    src/widgets/delegateutils/textselection.cpp
M  +7    -0    src/widgets/delegateutils/textselection.h
M  +47   -7    src/widgets/room/autotests/textselectiontest.cpp
M  +1    -0    src/widgets/room/autotests/textselectiontest.h

https://invent.kde.org/network/ruqola/-/commit/5c6747803e2770687e3ab4ff422fcb01952b85ae

diff --git a/src/widgets/delegateutils/textselection.cpp b/src/widgets/delegateutils/textselection.cpp
index bd3a45c1d8..1a20e88854 100644
--- a/src/widgets/delegateutils/textselection.cpp
+++ b/src/widgets/delegateutils/textselection.cpp
@@ -228,6 +228,35 @@ QTextCursor TextSelection::selectionForIndex(const QModelIndex &index, QTextDocu
         }
     }
 
+    if ((att.isValid() || msgUrl.hasHtmlDescription()) && mStartPos >= 0 && ordered.fromRow != ordered.toRow && fromCharPos == toCharPos) {
+        // If selection started in message text and now spans multiple rows,
+        // keep URL/attachment block visibly selected instead of a collapsed point.
+        fromCharPos = 0;
+        toCharPos = maxCharPos;
+    }
+
+    if (att.isValid() || msgUrl.hasHtmlDescription()) {
+        // Attachment/URL preview selection is always local to that document;
+        // don't reinterpret it through multi-row text boundaries.
+        cursor.setPosition(qBound(0, fromCharPos, maxCharPos));
+        cursor.setPosition(qBound(0, toCharPos, maxCharPos), QTextCursor::KeepAnchor);
+        return cursor;
+    }
+
+    if (mStartPos >= 0 && mEndSelectionArea != EndSelectionArea::Text && mStartIndex.isValid() && mEndIndex.isValid() && mStartIndex.row() != mEndIndex.row()
+        && index.row() == mStartIndex.row()) {
+        // Keep the original text-side selection on the start row while the endpoint is in URL/attachment.
+        cursor.setPosition(qBound(0, mStartPos, maxCharPos));
+        cursor.movePosition(QTextCursor::End, QTextCursor::KeepAnchor);
+        return cursor;
+    }
+
+    if (ordered.fromRow != ordered.toRow && mEndIndex.isValid() && index.row() == mEndIndex.row() && mEndSelectionArea != EndSelectionArea::Text) {
+        // Mouse endpoint is currently in attachment/URL preview on this row;
+        // don't select this row's main message text until endpoint enters text.
+        return {};
+    }
+
     // qDebug() << "AFTER toCharPos" << toCharPos << " fromCharPos " << fromCharPos;
     const int row = index.row();
     if (row == ordered.fromRow)
@@ -254,6 +283,7 @@ void TextSelection::clear()
     mEndIndex = QPersistentModelIndex{};
     mStartPos = -1;
     mEndPos = -1;
+    mEndSelectionArea = EndSelectionArea::Text;
     mAttachmentSelection.clear();
     mMessageUrlSelection.clear();
 
@@ -327,6 +357,7 @@ void TextSelection::setTextSelectionEnd(const QModelIndex &index, int charPos)
     Q_ASSERT(index.isValid());
     mEndIndex = index;
     mEndPos = charPos;
+    mEndSelectionArea = EndSelectionArea::Text;
 }
 
 void TextSelection::setAttachmentTextSelectionEnd(const QModelIndex &index, int charPos, const MessageAttachment &msgAttach)
@@ -340,6 +371,7 @@ void TextSelection::setAttachmentTextSelectionEnd(const QModelIndex &index, int
         // The drag endpoint is currently in attachment text; main text must stay unselected.
         mEndPos = -1;
     }
+    mEndSelectionArea = EndSelectionArea::Attachment;
     if (msgAttach.isValid()) {
         const auto countAtt{mAttachmentSelection.count()};
         for (int i = 0; i < countAtt; ++i) {
@@ -352,6 +384,7 @@ void TextSelection::setAttachmentTextSelectionEnd(const QModelIndex &index, int
         }
 
         AttachmentSelection selection;
+        selection.fromCharPos = keepTextSelectionEndPos ? charPos : 0;
         selection.toCharPos = charPos;
         selection.attachment = msgAttach;
         mAttachmentSelection.append(std::move(selection));
@@ -369,6 +402,7 @@ void TextSelection::setPreviewUrlTextSelectionEnd(const QModelIndex &index, int
         // The drag endpoint is currently in URL preview text; main text must stay unselected.
         mEndPos = -1;
     }
+    mEndSelectionArea = EndSelectionArea::MessageUrl;
     if (msgUrl.hasHtmlDescription()) {
         const auto countMessageUrl{mMessageUrlSelection.count()};
         for (int i = 0; i < countMessageUrl; ++i) {
@@ -381,6 +415,7 @@ void TextSelection::setPreviewUrlTextSelectionEnd(const QModelIndex &index, int
         }
 
         MessageUrlSelection selection;
+        selection.fromCharPos = keepTextSelectionEndPos ? charPos : 0;
         selection.toCharPos = charPos;
         selection.messageUrl = msgUrl;
         mMessageUrlSelection.append(std::move(selection));
diff --git a/src/widgets/delegateutils/textselection.h b/src/widgets/delegateutils/textselection.h
index 930fe0ee0c..9caf95a2a0 100644
--- a/src/widgets/delegateutils/textselection.h
+++ b/src/widgets/delegateutils/textselection.h
@@ -113,6 +113,12 @@ private:
         int toCharPos = 0;
     };
 
+    enum class EndSelectionArea : uint8_t {
+        Text,
+        Attachment,
+        MessageUrl,
+    };
+
     [[nodiscard]] LIBRUQOLAWIDGETS_NO_EXPORT OrderedPositions orderedPositions() const;
     LIBRUQOLAWIDGETS_NO_EXPORT void selectionText(const OrderedPositions ordered,
                                                   Format format,
@@ -129,6 +135,7 @@ private:
     QList<MessageUrlSelection> mMessageUrlSelection;
     int mStartPos = -1; // first selected character in start row
     int mEndPos = -1; // last selected character in end row
+    EndSelectionArea mEndSelectionArea = EndSelectionArea::Text;
 
     DocumentFactoryInterface *mTextHelperFactory = nullptr;
     DocumentFactoryInterface *mMessageUrlHelperFactory = nullptr;
diff --git a/src/widgets/room/autotests/textselectiontest.cpp b/src/widgets/room/autotests/textselectiontest.cpp
index e9eb021467..9885482256 100644
--- a/src/widgets/room/autotests/textselectiontest.cpp
+++ b/src/widgets/room/autotests/textselectiontest.cpp
@@ -275,6 +275,45 @@ void TextSelectionTest::testSelectionExtendingToUrlPreviewKeepsTextSelection()
 
     const QTextCursor urlCursor = selection.selectionForIndex(index1, &urlPreviewDoc, {}, messageUrl);
     QVERIFY(!urlCursor.isNull());
+    QCOMPARE(urlCursor.position(), urlCursor.anchor());
+}
+
+void TextSelectionTest::testSelectionFromTextToUrlDoesNotSelectUrlRowTextUntilTextIsHit()
+{
+    const QModelIndex index1 = model.index(1, 0);
+    const QModelIndex index2 = model.index(2, 0);
+    TestFactory factory(model.rowCount());
+    TextSelection selection;
+    selection.setTextHelperFactory(&factory);
+
+    MessageUrl messageUrl;
+    messageUrl.setUrl(u"https://kde.org"_s);
+    messageUrl.setPageTitle(u"KDE"_s);
+    messageUrl.setDescription(u"Community"_s);
+    messageUrl.generateMessageUrlInfo();
+    QVERIFY(messageUrl.hasHtmlDescription());
+
+    // Start from message text in a lower row.
+    selection.setTextSelectionStart(index2, 0);
+    selection.setTextSelectionEnd(index2, 11);
+
+    // Move endpoint up into URL preview (same row as index1 message).
+    selection.setPreviewUrlTextSelectionEnd(index1, 2, messageUrl);
+    selection.setPreviewUrlTextSelectionEnd(index1, 12, messageUrl);
+
+    const QTextCursor row2Cursor = selection.selectionForIndex(index2, factory.documentForIndex(index2));
+    QVERIFY(!row2Cursor.isNull());
+    QCOMPARE(row2Cursor.selection().toPlainText(), u"Line 2 bold"_s);
+
+    // While endpoint is in URL preview on row 1, row 1 message text must stay unselected.
+    const QTextCursor row1Cursor = selection.selectionForIndex(index1, factory.documentForIndex(index1));
+    QVERIFY(row1Cursor.isNull() || row1Cursor.selection().toPlainText().isEmpty());
+
+    QTextDocument urlPreviewDoc;
+    urlPreviewDoc.setHtml(messageUrl.htmlDescription());
+    const QTextCursor urlCursor = selection.selectionForIndex(index1, &urlPreviewDoc, {}, messageUrl);
+    QVERIFY(!urlCursor.isNull());
+    QVERIFY(!urlCursor.selection().toPlainText().isEmpty());
 }
 
 void TextSelectionTest::testSelectionStartingInUrlPreviewAndMovingToText()
@@ -312,7 +351,7 @@ void TextSelectionTest::testSelectionStartingInUrlPreviewAndMovingToText()
 void TextSelectionTest::testSelectionStartingInUrlPreviewAndMovingToPreviousMessage()
 {
     const QModelIndex index0 = model.index(0, 0);
-    const QModelIndex index1 = model.index(1, 0);
+    const QModelIndex index2 = model.index(2, 0);
     TestFactory factory(model.rowCount());
     TextSelection selection;
     selection.setTextHelperFactory(&factory);
@@ -324,22 +363,23 @@ void TextSelectionTest::testSelectionStartingInUrlPreviewAndMovingToPreviousMess
     messageUrl.generateMessageUrlInfo();
     QVERIFY(messageUrl.hasHtmlDescription());
 
-    selection.setPreviewUrlTextSelectionStart(index1, 366, messageUrl);
+    selection.setPreviewUrlTextSelectionStart(index2, 2, messageUrl);
+    selection.setPreviewUrlTextSelectionEnd(index2, 12, messageUrl);
 
-    // Move the selection endpoint to the previous message row.
+    // Move the selection endpoint two rows up.
     selection.setTextSelectionEnd(index0, 4);
 
     const QTextCursor row0Cursor = selection.selectionForIndex(index0, factory.documentForIndex(index0));
     QVERIFY(!row0Cursor.isNull());
     QCOMPARE(row0Cursor.selection().toPlainText(), u" 0"_s);
 
-    const QTextCursor row1Cursor = selection.selectionForIndex(index1, factory.documentForIndex(index1));
-    QVERIFY(!row1Cursor.isNull());
-    QCOMPARE(row1Cursor.selection().toPlainText(), u"Line 1 bold"_s);
+    const QTextCursor row2Cursor = selection.selectionForIndex(index2, factory.documentForIndex(index2));
+    QVERIFY(!row2Cursor.isNull());
+    QCOMPARE(row2Cursor.selection().toPlainText(), u"Line 2 bold"_s);
 
     QTextDocument urlPreviewDoc;
     urlPreviewDoc.setHtml(messageUrl.htmlDescription());
-    const QTextCursor urlCursor = selection.selectionForIndex(index1, &urlPreviewDoc, {}, messageUrl);
+    const QTextCursor urlCursor = selection.selectionForIndex(index2, &urlPreviewDoc, {}, messageUrl);
     QVERIFY(!urlCursor.isNull());
     QVERIFY(!urlCursor.selection().toPlainText().isEmpty());
 }
diff --git a/src/widgets/room/autotests/textselectiontest.h b/src/widgets/room/autotests/textselectiontest.h
index daea08e110..0e3fdba6b9 100644
--- a/src/widgets/room/autotests/textselectiontest.h
+++ b/src/widgets/room/autotests/textselectiontest.h
@@ -25,6 +25,7 @@ private Q_SLOTS:
     void testSelectAll();
     void testSelectionForIndexDoesNotIncludeUrlPreviewByDefault();
     void testSelectionExtendingToUrlPreviewKeepsTextSelection();
+    void testSelectionFromTextToUrlDoesNotSelectUrlRowTextUntilTextIsHit();
     void testSelectionStartingInUrlPreviewAndMovingToText();
     void testSelectionStartingInUrlPreviewAndMovingToPreviousMessage();
     void textClear();
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.