gui: use TapHandler for double-click pair selection

MouseArea's onDoubleClicked doesn't fire when placed on a TextEdit with
selectByMouse:true — the TextEdit's internal mouse handling consumes the
events. TapHandler coexists with TextEdit's mouse handling and reliably
receives double-tap events.

Double-click on a bracket or quote delimiter now selects the content
between the matching pair. Quotes use parity to decide opener/closer
direction. Falls back to word selection when no delimiter is adjacent.

Works in chat blocks (mainTextEdit, textComponent) and prompt textarea.
This commit is contained in:
Levi Neely 2026-10-08 17:57:40 +02:00
parent 840723de3b
commit 8147b3885f
2 changed files with 50 additions and 83 deletions

View File

@ -193,14 +193,10 @@ Column {
// Fast path: single TextEdit for the concatenated HTML.
// Diagrams are handled separately via the segments list.
// Editable for scratchpad use - changes are local only, not persisted.
Item {
width: root.width
height: mainTextEdit.height
visible: !hasDiagrams()
TextEdit {
id: mainTextEdit
width: parent.width
width: root.width
visible: !hasDiagrams()
text: root.blockRenderedHtml
textFormat: Text.RichText
selectByMouse: true
@ -210,35 +206,26 @@ Column {
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
selectionColor: theme.highlightColor
selectedTextColor: theme.highlightedTextColor
// Double-click: bracket/quote pair or word select
TapHandler {
acceptedButtons: Qt.LeftButton
onDoubleTapped: function(eventPoint, button) {
var pos = eventPoint.position
if (!TextSelection.selectBracketPair(mainTextEdit, pos.x, pos.y)) {
TextSelection.selectWordAt(mainTextEdit, pos.x, pos.y)
}
}
}
// Acme-style mouse handling: B2=execute, B3=plumb
// B1+B2=cut, B1+B3=paste (chord detection)
// Double-click on bracket: select text between matching pair
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
z: 1
onDoubleClicked: function(mouse) {
if (mouse.button === Qt.LeftButton) {
// Delimiter under cursor: select the pair.
// Otherwise select the word ourselves (the overlay
// swallows the event, so TextEdit won't do it).
if (!TextSelection.selectBracketPair(mainTextEdit, mouse.x, mouse.y)) {
TextSelection.selectWordAt(mainTextEdit, mouse.x, mouse.y)
}
mouse.accepted = true
}
}
acceptedButtons: Qt.MiddleButton | Qt.RightButton
cursorShape: Qt.IBeamCursor
onPressed: function(mouse) {
// Left button single clicks pass through to TextEdit for selection
if (mouse.button === Qt.LeftButton) {
mouse.accepted = false
return
}
if (mouse.buttons & Qt.LeftButton) {
if (mouse.button === Qt.MiddleButton) {
mainTextEdit.cut()
@ -261,7 +248,6 @@ Column {
if (sel && sel.trim() !== "") {
plumber.plumb(sel.trim(), sessionCwd())
} else {
// No selection - expand word and highlight it
selectAndPlumb(mainTextEdit, mouse.x, mouse.y)
}
}
@ -401,29 +387,25 @@ Column {
selectionColor: theme.highlightColor
selectedTextColor: theme.highlightedTextColor
// Acme-style mouse handling with chord support
// Double-click: bracket/quote pair or word select
TapHandler {
acceptedButtons: Qt.LeftButton
onDoubleTapped: function(eventPoint, button) {
var pos = eventPoint.position
if (!TextSelection.selectBracketPair(textEdit, pos.x, pos.y)) {
TextSelection.selectWordAt(textEdit, pos.x, pos.y)
}
}
}
// Acme-style mouse handling: B2=execute, B3=plumb
// B1+B2=cut, B1+B3=paste (chord detection)
// Double-click on bracket: select text between matching pair
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
acceptedButtons: Qt.MiddleButton | Qt.RightButton
cursorShape: Qt.IBeamCursor
onDoubleClicked: function(mouse) {
if (mouse.button === Qt.LeftButton) {
if (!TextSelection.selectBracketPair(textEdit, mouse.x, mouse.y)) {
TextSelection.selectWordAt(textEdit, mouse.x, mouse.y)
}
mouse.accepted = true
}
}
onPressed: function(mouse) {
if (mouse.button === Qt.LeftButton) {
mouse.accepted = false
return
}
if (mouse.buttons & Qt.LeftButton) {
if (mouse.button === Qt.MiddleButton) {
textEdit.cut()
@ -441,7 +423,6 @@ Column {
}
mouse.accepted = true
} else if (mouse.button === Qt.RightButton) {
// B3: Plumb selection or word under cursor
if (plumber.available()) {
var sel = textEdit.selectedText
if (sel && sel.trim() !== "") {

View File

@ -467,11 +467,21 @@ Item {
event.accepted = true
}
}
// Double-click: bracket/quote pair or word select
TapHandler {
acceptedButtons: Qt.LeftButton
onDoubleTapped: function(eventPoint, button) {
var pos = eventPoint.position
if (!TextSelection.selectBracketPair(promptInput, pos.x, pos.y)) {
TextSelection.selectWordAt(promptInput, pos.x, pos.y)
}
}
}
}
}
// Acme-style mouse chording overlay
// B1 double-click = bracket/quote pair or word select
// B2 = execute selection
// B3 = plumb selection / context menu
// B1+B2 = cut (snarf)
@ -479,67 +489,43 @@ Item {
MouseArea {
id: chordArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
acceptedButtons: Qt.MiddleButton | Qt.RightButton
propagateComposedEvents: true
cursorShape: Qt.IBeamCursor
// Track if B1 (left) is currently held for chord detection
property bool leftHeld: false
onDoubleClicked: function(mouse) {
if (mouse.button === Qt.LeftButton) {
// Map coordinates from chordArea to promptInput
var mappedPos = chordArea.mapToItem(promptInput, mouse.x, mouse.y)
if (!TextSelection.selectBracketPair(promptInput, mappedPos.x, mappedPos.y)) {
TextSelection.selectWordAt(promptInput, mappedPos.x, mappedPos.y)
}
mouse.accepted = true
}
}
onPressed: function(mouse) {
// Left button single clicks pass through to TextArea for selection
if (mouse.button === Qt.LeftButton) {
mouse.accepted = false
return
}
// Check if this is a chord (B1 already held)
if (mouse.buttons & Qt.LeftButton) {
leftHeld = true
if (mouse.button === Qt.MiddleButton) {
// B1+B2 = cut
promptInput.cut()
mouse.accepted = true
return
} else if (mouse.button === Qt.RightButton) {
// B1+B3 = paste
promptInput.paste()
mouse.accepted = true
return
}
}
leftHeld = false
mouse.accepted = false // Let it propagate for non-chord
mouse.accepted = false
}
onClicked: function(mouse) {
// Only handle non-chord clicks
if (leftHeld) {
leftHeld = false
return
}
if (mouse.button === Qt.MiddleButton) {
// B2: Execute selection as prompt
var sel = promptInput.selectedText
if (sel && sel.trim() !== "") {
ollie.submit(sel.trim())
} else {
// No selection - ignore
}
} else if (mouse.button === Qt.RightButton) {
// B3: Plumb selection or word under cursor
if (!plumber.available()) return
var sel = promptInput.selectedText
if (sel && sel.trim() !== "") {