diff --git a/src/qml/Component/Timeline/MessageDelegate.qml b/src/qml/Component/Timeline/MessageDelegate.qml index 9e2e7e986..13580bd7f 100644 --- a/src/qml/Component/Timeline/MessageDelegate.qml +++ b/src/qml/Component/Timeline/MessageDelegate.qml @@ -13,6 +13,13 @@ import org.kde.neochat 1.0 TimelineContainer { id: messageDelegate + function positionAt(x, y) { + let point = label.mapFromItem(messageDelegate, x, y) + return label.positionAt(point.x, point.y) + } + + property alias selectedText: label.selectedText + property bool isEmote: false onOpenContextMenu: openMessageContext(model, label.selectedText, Controller.plainText(label.textDocument)) @@ -22,6 +29,7 @@ TimelineContainer { id: label Layout.fillWidth: true isEmote: messageDelegate.isEmote + isDelegate: true } Loader { id: linkPreviewLoader diff --git a/src/qml/Component/Timeline/RichLabel.qml b/src/qml/Component/Timeline/RichLabel.qml index baf7c2a8c..8dc50b65b 100644 --- a/src/qml/Component/Timeline/RichLabel.qml +++ b/src/qml/Component/Timeline/RichLabel.qml @@ -16,6 +16,8 @@ TextEdit { property bool isEmote: false + property bool isDelegate: false + readonly property var linkRegex: /(href=["'])?(\b(https?):\/\/[^\s\<\>\"\'\\]+)/g property string textMessage: model.display.includes("http") ? model.display.replace(linkRegex, function() { @@ -41,8 +43,11 @@ TextEdit { persistentSelection: true // Work around QTBUG 93281 - Component.onCompleted: if (text.includes(" @@ -103,4 +108,26 @@ a{ enabled: !parent.hoveredLink && !spoilerRevealed onTapped: spoilerRevealed = true } + + Connections { + target: selectionArea + enabled: contentLabel.isDelegate + function onSelectionChanged() { + updateSelection(); + } + } + + function updateSelection() { + if (index < selectionArea.lowerIndex || index > selectionArea.upperIndex) { + contentLabel.select(0, 0); + } else if (index > selectionArea.lowerIndex && index < selectionArea.upperIndex) { + contentLabel.selectAll(); + } else if (index === selectionArea.selectionStartIndex && index === selectionArea.selectionEndIndex) { + contentLabel.select(selectionArea.upperPos, selectionArea.lowerPos); + } else if (index === selectionArea.upperIndex) { + contentLabel.select(selectionArea.upperPos, contentLabel.length); + } else if (index === selectionArea.lowerIndex) { + contentLabel.select(0, selectionArea.lowerPos); + } + } } diff --git a/src/qml/Page/RoomPage.qml b/src/qml/Page/RoomPage.qml index b3a0875de..aae8fe0aa 100644 --- a/src/qml/Page/RoomPage.qml +++ b/src/qml/Page/RoomPage.qml @@ -549,8 +549,66 @@ Kirigami.ScrollablePage { } } } - } + function indexAtRelative(x, y) { + return indexAt(x + contentX, y + contentY) + } + + MouseArea { + id: selectionArea + property int selectionStartIndex + property int selectionEndIndex + property int selectionStartPos + property int selectionEndPos + + property int upperIndex: selectionStartIndex > selectionEndIndex ? selectionStartIndex : selectionEndIndex + property int upperPos: selectionStartIndex > selectionEndIndex ? selectionStartPos : (selectionStartIndex == selectionEndIndex ? (selectionStartPos > selectionEndPos ? selectionEndPos : selectionStartPos) : selectionEndPos) + property int lowerIndex: selectionStartIndex > selectionEndIndex ? selectionEndIndex : selectionStartIndex + property int lowerPos: selectionStartIndex > selectionEndIndex ? selectionEndPos : (selectionStartIndex == selectionEndIndex ? (selectionStartPos > selectionEndPos ? selectionStartPos : selectionEndPos) : selectionStartPos) + + signal selectionChanged + + anchors.fill: parent + + function indexAndPos(x, y) { + const index = messageListView.indexAtRelative(x, y); + if (index == -1) { + return; + } + const item = messageListView.itemAtIndex(index); + const relItemY = item.y - messageListView.contentY; + const pos = item.positionAt(x, y - relItemY); + + return [index, pos] + } + + onPressed: { + [selectionEndIndex, selectionEndPos] = indexAndPos(mouse.x, mouse.y); + [selectionStartIndex, selectionStartPos] = indexAndPos(mouse.x, mouse.y); + selectionChanged(); + } + onPositionChanged: { + if (!pressed) { + return + } + [selectionEndIndex, selectionEndPos] = indexAndPos(mouse.x, mouse.y); + selectionChanged(); + } + } + Kirigami.Action { + onTriggered: { + var text = "" + for (let i = selectionArea.upperIndex; i >= selectionArea.lowerIndex; i--) { + text += messageListView.itemAtIndex(i).selectedText + if (i > selectionArea.lowerIndex) { + text += " " + } + } + Clipboard.saveText(text) + } + shortcut: "Ctrl+C" + } + } footer: ChatBox { id: chatBox