gui: bracket-pair selection on double-click
Double-clicking next to a bracket (()[]{}) selects the text between
the matching pair, including nested pairs. Falls through to the
default word-select when no bracket is adjacent.
Also: B3-click on word now highlights it before plumbing.
This commit is contained in:
parent
36e8b0abd3
commit
1cfa424220
|
|
@ -192,35 +192,55 @@ 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.
|
||||
TextEdit {
|
||||
id: mainTextEdit
|
||||
Item {
|
||||
width: root.width
|
||||
height: mainTextEdit.height
|
||||
visible: !hasDiagrams()
|
||||
text: root.blockRenderedHtml
|
||||
textFormat: Text.RichText
|
||||
selectByMouse: true
|
||||
persistentSelection: true
|
||||
wrapMode: TextEdit.Wrap
|
||||
font.family: theme.fontFamily || Qt.application.font.family
|
||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
|
||||
selectionColor: theme.highlightColor
|
||||
selectedTextColor: theme.highlightedTextColor
|
||||
|
||||
// Acme-style mouse handling: B2=execute, B3=plumb/menu
|
||||
TextEdit {
|
||||
id: mainTextEdit
|
||||
anchors.fill: parent
|
||||
text: root.blockRenderedHtml
|
||||
textFormat: Text.RichText
|
||||
selectByMouse: true
|
||||
persistentSelection: true
|
||||
wrapMode: TextEdit.Wrap
|
||||
font.family: theme.fontFamily || Qt.application.font.family
|
||||
font.pointSize: theme.fontSize > 0 ? theme.fontSize : 12
|
||||
selectionColor: theme.highlightColor
|
||||
selectedTextColor: theme.highlightedTextColor
|
||||
}
|
||||
|
||||
// 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.MiddleButton | Qt.RightButton
|
||||
cursorShape: Qt.IBeamCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
z: 1
|
||||
|
||||
onDoubleClicked: function(mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
// Try bracket-pair selection; fall through to word select if no bracket
|
||||
if (selectBracketPair(mainTextEdit, mouse.x, mouse.y)) {
|
||||
mouse.accepted = true
|
||||
} else {
|
||||
mouse.accepted = false // let TextEdit do word select
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: function(mouse) {
|
||||
// Check if this is a chord (B1 already held)
|
||||
// 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) {
|
||||
// B1+B2 = cut (snarf)
|
||||
mainTextEdit.cut()
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
// B1+B3 = paste
|
||||
mainTextEdit.paste()
|
||||
}
|
||||
mouse.accepted = true
|
||||
|
|
@ -228,23 +248,19 @@ Column {
|
|||
}
|
||||
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
// B2: Execute selection as prompt
|
||||
var sel = mainTextEdit.selectedText
|
||||
if (sel && sel.trim() !== "") {
|
||||
executeText(sel)
|
||||
}
|
||||
mouse.accepted = true
|
||||
} else if (mouse.button === Qt.RightButton) {
|
||||
// B3: Plumb selection or word under cursor
|
||||
if (plumber.available()) {
|
||||
var sel = mainTextEdit.selectedText
|
||||
if (sel && sel.trim() !== "") {
|
||||
plumber.plumb(sel.trim(), sessionCwd())
|
||||
} else {
|
||||
// No selection - expand word at click position (acme-style)
|
||||
var pos = mainTextEdit.positionAt(mouse.x, mouse.y)
|
||||
var word = wordAtCursor(mainTextEdit, pos)
|
||||
if (word) plumber.plumb(word, sessionCwd())
|
||||
// No selection - expand word and highlight it
|
||||
selectAndPlumb(mainTextEdit, mouse.x, mouse.y)
|
||||
}
|
||||
}
|
||||
mouse.accepted = true
|
||||
|
|
@ -329,16 +345,17 @@ Column {
|
|||
|
||||
// Expand cursor position to surrounding word/path for plumbing
|
||||
// Matches acme's isfilec: alphanumeric + . - + / : @
|
||||
// Returns {start, end, word} or null if no word found
|
||||
function wordAtCursor(textEdit, cursorPos) {
|
||||
var text = textEdit.getText(0, textEdit.length)
|
||||
if (cursorPos < 0 || cursorPos > text.length) return ""
|
||||
if (cursorPos < 0 || cursorPos > text.length) return null
|
||||
|
||||
// isfilec from acme: alphanumeric + . - + / : @
|
||||
function isfilec(ch) {
|
||||
return /[a-zA-Z0-9.\-+\/:@]/.test(ch)
|
||||
}
|
||||
|
||||
// Find start of word (acme also allows isaddrc chars going left)
|
||||
// Find start of word
|
||||
var start = cursorPos
|
||||
while (start > 0 && isfilec(text.charAt(start - 1))) {
|
||||
start--
|
||||
|
|
@ -350,8 +367,121 @@ Column {
|
|||
end++
|
||||
}
|
||||
|
||||
if (start === end) return ""
|
||||
return text.substring(start, end)
|
||||
if (start === end) return null
|
||||
return {start: start, end: end, word: text.substring(start, end)}
|
||||
}
|
||||
|
||||
// Select word at cursor and plumb it (with visual highlight)
|
||||
function selectAndPlumb(textEdit, mouseX, mouseY) {
|
||||
var pos = textEdit.positionAt(mouseX, mouseY)
|
||||
var result = wordAtCursor(textEdit, pos)
|
||||
if (result) {
|
||||
// Select the word to show what's being plumbed
|
||||
textEdit.select(result.start, result.end)
|
||||
plumber.plumb(result.word, sessionCwd())
|
||||
}
|
||||
}
|
||||
|
||||
// Bracket pairs for double-click selection
|
||||
function matchingBracket(ch) {
|
||||
switch (ch) {
|
||||
case '(': return {match: ')', isOpener: true}
|
||||
case '[': return {match: ']', isOpener: true}
|
||||
case '{': return {match: '}', isOpener: true}
|
||||
case ')': return {match: '(', isOpener: false}
|
||||
case ']': return {match: '[', isOpener: false}
|
||||
case '}': return {match: '{', isOpener: false}
|
||||
default: return null
|
||||
}
|
||||
}
|
||||
|
||||
// Find matching closer, handling nesting
|
||||
function findCloser(text, startPos, opener, closer) {
|
||||
var depth = 1
|
||||
var pos = startPos
|
||||
while (pos < text.length) {
|
||||
var ch = text.charAt(pos)
|
||||
if (ch === opener) depth++
|
||||
else if (ch === closer) {
|
||||
depth--
|
||||
if (depth === 0) return pos
|
||||
}
|
||||
pos++
|
||||
}
|
||||
return -1 // unbalanced
|
||||
}
|
||||
|
||||
// Find matching opener, handling nesting (scan backwards)
|
||||
function findOpener(text, startPos, opener, closer) {
|
||||
var depth = 1
|
||||
var pos = startPos
|
||||
while (pos >= 0) {
|
||||
var ch = text.charAt(pos)
|
||||
if (ch === closer) depth++
|
||||
else if (ch === opener) {
|
||||
depth--
|
||||
if (depth === 0) return pos
|
||||
}
|
||||
pos--
|
||||
}
|
||||
return -1 // unbalanced
|
||||
}
|
||||
|
||||
// Select text between matching brackets on double-click
|
||||
// Returns true if a bracket pair was found and selected
|
||||
function selectBracketPair(textEdit, mouseX, mouseY) {
|
||||
var cursorPos = textEdit.positionAt(mouseX, mouseY)
|
||||
var text = textEdit.getText(0, textEdit.length)
|
||||
|
||||
if (cursorPos < 0 || cursorPos > text.length) return false
|
||||
|
||||
// Check char to the right of cursor
|
||||
var rightCh = cursorPos < text.length ? text.charAt(cursorPos) : ''
|
||||
var rightMatch = matchingBracket(rightCh)
|
||||
|
||||
// Check char to the left of cursor
|
||||
var leftCh = cursorPos > 0 ? text.charAt(cursorPos - 1) : ''
|
||||
var leftMatch = matchingBracket(leftCh)
|
||||
|
||||
var innerStart = -1
|
||||
var innerEnd = -1
|
||||
|
||||
// Prefer opener to the right, then closer to the left
|
||||
if (rightMatch && rightMatch.isOpener) {
|
||||
// Opener to right: find closer
|
||||
var closerPos = findCloser(text, cursorPos + 1, rightCh, rightMatch.match)
|
||||
if (closerPos >= 0) {
|
||||
innerStart = cursorPos + 1
|
||||
innerEnd = closerPos
|
||||
}
|
||||
} else if (leftMatch && !leftMatch.isOpener) {
|
||||
// Closer to left: find opener
|
||||
var openerPos = findOpener(text, cursorPos - 2, leftMatch.match, leftCh)
|
||||
if (openerPos >= 0) {
|
||||
innerStart = openerPos + 1
|
||||
innerEnd = cursorPos - 1
|
||||
}
|
||||
} else if (rightMatch && !rightMatch.isOpener) {
|
||||
// Closer to right: find opener backwards
|
||||
var openerPos = findOpener(text, cursorPos - 1, rightMatch.match, rightCh)
|
||||
if (openerPos >= 0) {
|
||||
innerStart = openerPos + 1
|
||||
innerEnd = cursorPos
|
||||
}
|
||||
} else if (leftMatch && leftMatch.isOpener) {
|
||||
// Opener to left: find closer
|
||||
var closerPos = findCloser(text, cursorPos, leftCh, leftMatch.match)
|
||||
if (closerPos >= 0) {
|
||||
innerStart = cursorPos
|
||||
innerEnd = closerPos
|
||||
}
|
||||
}
|
||||
|
||||
if (innerStart >= 0 && innerEnd >= innerStart) {
|
||||
textEdit.select(innerStart, innerEnd)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Text segment component
|
||||
|
|
@ -373,12 +503,28 @@ Column {
|
|||
|
||||
// Acme-style mouse handling with chord support
|
||||
// B1+B2=cut, B1+B3=paste (chord detection)
|
||||
// Double-click on bracket: select text between matching pair
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.MiddleButton | Qt.RightButton
|
||||
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
|
||||
cursorShape: Qt.IBeamCursor
|
||||
|
||||
onDoubleClicked: function(mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
if (selectBracketPair(textEdit, mouse.x, mouse.y)) {
|
||||
mouse.accepted = true
|
||||
} else {
|
||||
mouse.accepted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: function(mouse) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
mouse.accepted = false
|
||||
return
|
||||
}
|
||||
|
||||
if (mouse.buttons & Qt.LeftButton) {
|
||||
if (mouse.button === Qt.MiddleButton) {
|
||||
textEdit.cut()
|
||||
|
|
@ -402,9 +548,7 @@ Column {
|
|||
if (sel && sel.trim() !== "") {
|
||||
plumber.plumb(sel.trim(), sessionCwd())
|
||||
} else {
|
||||
var pos = textEdit.positionAt(mouse.x, mouse.y)
|
||||
var word = wordAtCursor(textEdit, pos)
|
||||
if (word) plumber.plumb(word, sessionCwd())
|
||||
selectAndPlumb(textEdit, mouse.x, mouse.y)
|
||||
}
|
||||
}
|
||||
mouse.accepted = true
|
||||
|
|
|
|||
Loading…
Reference in New Issue