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
This commit is contained in:
Levi Neely 2026-08-07 11:00:15 +02:00
parent 624b7ad66d
commit f086400bf3
1 changed files with 100 additions and 5 deletions

View File

@ -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()
}
}
}
}
}