From f086400bf392793e0a2a2412136e2c839105c6a5 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Fri, 7 Aug 2026 11:00:15 +0200 Subject: [PATCH] gui: add text selection and context menus Chat blocks: - Enable selectByMouse and persistentSelection for text selection - Add context menu with Copy and Select All - Disable ListView drag during text selection (preserves wheel scroll) - I-beam cursor over text content Prompt input: - Enable selectByMouse and persistentSelection - Add context menu with Cut, Copy, Paste, Select All - Remove ScrollView wrapper (TextArea handles overflow natively) - I-beam cursor --- gui/main.qml | 105 ++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 100 insertions(+), 5 deletions(-) diff --git a/gui/main.qml b/gui/main.qml index 8d62fcd..714edc5 100644 --- a/gui/main.qml +++ b/gui/main.qml @@ -1375,16 +1375,78 @@ property bool blockExpanded: { // Block content is Markdown rendered by ChatBlockModel. TextEdit { + id: blockContent Layout.fillWidth: true visible: blockItem.blockExpanded text: blockRenderedHtml || "" textFormat: Text.RichText readOnly: true + selectByMouse: true + persistentSelection: true wrapMode: TextEdit.Wrap font.family: theme.fontFamily || Qt.application.font.family font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12 // Note: don't set 'color' here — it can override inline styles from // syntax highlighting in Qt5. The HTML content should set colors. + + // Selection colors + selectionColor: theme.highlightColor + selectedTextColor: theme.highlightedTextColor + + // Restore ListView scrolling when focus is lost + onActiveFocusChanged: { + if (!activeFocus) + chatView.interactive = true + } + + // Context menu for text operations + Menu { + id: textContextMenu + MenuItem { + text: "Copy" + enabled: blockContent.selectedText.length > 0 + onTriggered: blockContent.copy() + } + MenuItem { + text: "Select All" + onTriggered: blockContent.selectAll() + } + } + + // Mouse handling for selection (left button) + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.LeftButton + propagateComposedEvents: true + cursorShape: Qt.IBeamCursor + + onPressed: function(mouse) { + chatView.interactive = false + mouse.accepted = false // Let TextEdit handle selection + } + onReleased: function(mouse) { + chatView.interactive = true + mouse.accepted = false + } + onWheel: function(wheel) { + // Always allow wheel scrolling, even during selection + chatView.contentY = Math.max(0, Math.min( + chatView.contentHeight - chatView.height, + chatView.contentY - wheel.angleDelta.y + )) + wheel.accepted = true + } + } + + // Context menu (right button) - separate to avoid deselection + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + + onClicked: function(mouse) { + textContextMenu.popup() + } + } } } } @@ -1398,16 +1460,15 @@ property bool blockExpanded: { Layout.margins: 6 spacing: 4 - ScrollView { - id: promptScroll + // TextArea without ScrollView - wheel scrolls when content overflows + Item { Layout.fillWidth: true Layout.maximumHeight: 200 Layout.preferredHeight: Math.min(promptInput.contentHeight + 16, 200) - clip: true - + TextArea { id: promptInput - Layout.fillWidth: true + anchors.fill: parent placeholderText: { if (!ollie.daemonConnected) return "Not connected" if (!ollie.activeSessionId) return "No session" @@ -1417,6 +1478,8 @@ property bool blockExpanded: { } enabled: ollie.daemonConnected && ollie.activeSessionId !== "" && ollie.activeAgentId !== "" && !ollie.isSessionPaused(ollie.activeSessionId) wrapMode: TextEdit.Wrap + selectByMouse: true + persistentSelection: true property string _lastText: "" property int _lastCursorPos: 0 @@ -1438,6 +1501,38 @@ property bool blockExpanded: { } } } + + // Context menu overlay - only catches right-click + MouseArea { + anchors.fill: parent + acceptedButtons: Qt.RightButton + cursorShape: Qt.IBeamCursor + onClicked: promptContextMenu.popup() + } + + Menu { + id: promptContextMenu + MenuItem { + text: "Cut" + enabled: promptInput.selectedText.length > 0 + onTriggered: promptInput.cut() + } + MenuItem { + text: "Copy" + enabled: promptInput.selectedText.length > 0 + onTriggered: promptInput.copy() + } + MenuItem { + text: "Paste" + enabled: promptInput.canPaste + onTriggered: promptInput.paste() + } + MenuSeparator {} + MenuItem { + text: "Select All" + onTriggered: promptInput.selectAll() + } + } } } }