Scroll to the body segment that contains a link offset
Link reveal previously scrolled only to the section top (offset > 0 just chose Beginning vs Contain). Thread the body offset through to the rendered segment so the view scrolls to the matched segment, keeping the existing segmented body rendering (text/code/table/list). - The bodyParts builder now records each segment's startLine (the body line index where it begins) for text, list, code, and table parts, tracked through the line-categorizing loop and pushText. - A new delegate function offsetToY(bodyOffset) converts the offset to a body line, finds the last segment whose startLine <= that line, and returns that segment's y relative to the delegate via mapToItem. - On sectionRevealed with offset > 0, position the row at the viewport top (which realizes the delegate), then nudge contentY by offsetToY so the matched segment sits at the top, clamped to the content bounds. The offset->line->segment selection was verified with a standalone harness replicating the builder and selection logic: for a body mixing intro text, a code block, a paragraph, a table, trailing text, and a list, every line maps to the segment that actually contains it. The final pixel scroll uses standard Qt Quick coordinate mapping; it needs a rendered scene to verify visually, which is not possible headlessly. Granularity is now segment-precise; intra-segment line precision is not attempted (text segments are Labels without glyph positioning).
This commit is contained in:
parent
de43a3721d
commit
bfc0c80d09
|
|
@ -81,18 +81,31 @@ ListView {
|
|||
property int flashRow: -1
|
||||
|
||||
// When the model reveals a section, select it, scroll it into view, and
|
||||
// flash it briefly. `bodyOffset` is a byte offset into the section's body:
|
||||
// when the match lies inside the body (offset > 0), position the row at the
|
||||
// top of the view so as much of the body as possible is visible; otherwise
|
||||
// just ensure the row is contained.
|
||||
// flash it briefly. `bodyOffset` is a byte offset into the section's body.
|
||||
// When the match lies inside the body (offset > 0), position the row at the
|
||||
// top of the view, then scroll further so the body segment containing the
|
||||
// offset sits at the top (segment-precise). Otherwise just ensure the row
|
||||
// is contained.
|
||||
Connections {
|
||||
target: docModel
|
||||
function onSectionRevealed(row, bodyOffset) {
|
||||
if (row < 0)
|
||||
return
|
||||
outline.currentIndex = row
|
||||
outline.positionViewAtIndex(
|
||||
row, bodyOffset > 0 ? ListView.Beginning : ListView.Contain)
|
||||
if (bodyOffset > 0) {
|
||||
// Bring the row's top to the viewport top first; this also
|
||||
// realizes the delegate so its segments can be measured.
|
||||
outline.positionViewAtIndex(row, ListView.Beginning)
|
||||
let item = outline.itemAtIndex(row)
|
||||
if (item && item.offsetToY) {
|
||||
let extra = item.offsetToY(bodyOffset)
|
||||
let maxY = Math.max(outline.originY,
|
||||
outline.contentHeight - outline.height)
|
||||
outline.contentY = Math.min(outline.contentY + extra, maxY)
|
||||
}
|
||||
} else {
|
||||
outline.positionViewAtIndex(row, ListView.Contain)
|
||||
}
|
||||
outline.flashRow = row
|
||||
flashTimer.restart()
|
||||
}
|
||||
|
|
@ -169,34 +182,42 @@ ListView {
|
|||
// dedicated "list" parts so they render with bullets/checkboxes.
|
||||
// `cbCounter` is a single-element array used as a mutable counter so
|
||||
// checkbox ordinals are stable across the whole section body.
|
||||
function pushText(text, cbCounter) {
|
||||
// `baseLine` is the body line index of the first line of `text`,
|
||||
// so each emitted part records the body line where it begins
|
||||
// (used for char/offset-precise scrolling).
|
||||
function pushText(text, cbCounter, baseLine) {
|
||||
if (!text) return;
|
||||
let lines = text.split('\n');
|
||||
let buf = [];
|
||||
let items = [];
|
||||
let bufStart = -1;
|
||||
let itemsStart = -1;
|
||||
function flushBuf() {
|
||||
if (buf.length > 0) {
|
||||
let t = buf.join('\n').trim();
|
||||
if (t) parts.push({type: "text", content: t});
|
||||
if (t) parts.push({type: "text", content: t, startLine: bufStart});
|
||||
buf = [];
|
||||
}
|
||||
}
|
||||
function flushItems() {
|
||||
if (items.length > 0) {
|
||||
parts.push({type: "list", items: items});
|
||||
parts.push({type: "list", items: items, startLine: itemsStart});
|
||||
items = [];
|
||||
}
|
||||
}
|
||||
for (let ln of lines) {
|
||||
for (let li = 0; li < lines.length; li++) {
|
||||
let ln = lines[li];
|
||||
let item = parseListItem(ln);
|
||||
if (item) {
|
||||
flushBuf();
|
||||
if (items.length === 0) itemsStart = baseLine + li;
|
||||
// Assign a stable ordinal to checkbox-bearing items.
|
||||
item.checkboxOrdinal = (item.checkbox !== "")
|
||||
? cbCounter[0]++ : -1;
|
||||
items.push(item);
|
||||
} else {
|
||||
flushItems();
|
||||
if (buf.length === 0) bufStart = baseLine + li;
|
||||
buf.push(ln);
|
||||
}
|
||||
}
|
||||
|
|
@ -209,12 +230,21 @@ ListView {
|
|||
|
||||
let lines = body.split('\n');
|
||||
let currentText = [];
|
||||
let textRunStart = -1; // body line index where currentText began
|
||||
let inCode = false;
|
||||
let codeLang = "";
|
||||
let codeLines = [];
|
||||
let codeStart = -1; // body line index of the #+BEGIN_SRC
|
||||
let codeCounter = 0;
|
||||
let inTable = false;
|
||||
let tableLines = [];
|
||||
let tableStart = -1; // body line index of the first table row
|
||||
|
||||
// Append a line to the pending text run, recording its start line.
|
||||
function pushCurrentText(i) {
|
||||
if (currentText.length === 0) textRunStart = i;
|
||||
currentText.push(lines[i]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
let line = lines[i];
|
||||
|
|
@ -224,14 +254,15 @@ ListView {
|
|||
if (!inCode && !inTable && trimmed.toUpperCase().startsWith("#+BEGIN_SRC")) {
|
||||
// Flush pending text
|
||||
if (currentText.length > 0) {
|
||||
pushText(currentText.join('\n').trim(), cbCounter);
|
||||
pushText(currentText.join('\n').trim(), cbCounter, textRunStart);
|
||||
currentText = [];
|
||||
}
|
||||
inCode = true;
|
||||
codeStart = i;
|
||||
codeLang = trimmed.substring(11).trim().split(/\s+/)[0] || "";
|
||||
codeLines = [];
|
||||
} else if (inCode && trimmed.toUpperCase() === "#+END_SRC") {
|
||||
parts.push({type: "code", language: codeLang, content: codeLines.join('\n'), codeIndex: codeCounter});
|
||||
parts.push({type: "code", language: codeLang, content: codeLines.join('\n'), codeIndex: codeCounter, startLine: codeStart});
|
||||
codeCounter++;
|
||||
inCode = false;
|
||||
codeLang = "";
|
||||
|
|
@ -243,10 +274,11 @@ ListView {
|
|||
else if (!inTable && trimmed.startsWith("|")) {
|
||||
// Flush pending text
|
||||
if (currentText.length > 0) {
|
||||
pushText(currentText.join('\n').trim(), cbCounter);
|
||||
pushText(currentText.join('\n').trim(), cbCounter, textRunStart);
|
||||
currentText = [];
|
||||
}
|
||||
inTable = true;
|
||||
tableStart = i;
|
||||
tableLines = [trimmed];
|
||||
} else if (inTable && trimmed.startsWith("|")) {
|
||||
tableLines.push(trimmed);
|
||||
|
|
@ -264,21 +296,21 @@ ListView {
|
|||
rows.push(cells);
|
||||
}
|
||||
}
|
||||
parts.push({type: "table", rows: rows, hasHeader: hasHeader});
|
||||
parts.push({type: "table", rows: rows, hasHeader: hasHeader, startLine: tableStart});
|
||||
tableLines = [];
|
||||
inTable = false;
|
||||
|
||||
// Process current line as text if not empty
|
||||
if (trimmed) currentText.push(line);
|
||||
if (trimmed) pushCurrentText(i);
|
||||
}
|
||||
} else {
|
||||
currentText.push(line);
|
||||
pushCurrentText(i);
|
||||
}
|
||||
}
|
||||
|
||||
// Flush remaining
|
||||
if (inCode && codeLines.length > 0) {
|
||||
parts.push({type: "code", language: codeLang, content: codeLines.join('\n'), codeIndex: codeCounter});
|
||||
parts.push({type: "code", language: codeLang, content: codeLines.join('\n'), codeIndex: codeCounter, startLine: codeStart});
|
||||
codeCounter++;
|
||||
} else if (inTable && tableLines.length > 0) {
|
||||
let rows = [];
|
||||
|
|
@ -291,14 +323,45 @@ ListView {
|
|||
rows.push(cells);
|
||||
}
|
||||
}
|
||||
parts.push({type: "table", rows: rows, hasHeader: hasHeader});
|
||||
parts.push({type: "table", rows: rows, hasHeader: hasHeader, startLine: tableStart});
|
||||
} else if (currentText.length > 0) {
|
||||
pushText(currentText.join('\n').trim(), cbCounter);
|
||||
pushText(currentText.join('\n').trim(), cbCounter, textRunStart);
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
|
||||
// Map a body byte offset to a y position (relative to this delegate's
|
||||
// top) by locating the body segment that contains the offset's line and
|
||||
// returning that segment's rendered y. This keeps the segmented
|
||||
// rendering while scrolling precisely to the matched segment rather
|
||||
// than only the section top. Returns 0 when there is no better target.
|
||||
function offsetToY(bodyOffset) {
|
||||
if (bodyOffset <= 0 || bodyParts.length === 0)
|
||||
return 0;
|
||||
// Target line = number of newlines before the offset in the body.
|
||||
let body = sectionBody;
|
||||
let clamped = Math.min(bodyOffset, body.length);
|
||||
let targetLine = 0;
|
||||
for (let k = 0; k < clamped; k++)
|
||||
if (body.charCodeAt(k) === 10) targetLine++;
|
||||
// Find the last part whose startLine <= targetLine (parts are in
|
||||
// document order with non-decreasing startLine).
|
||||
let pIdx = -1;
|
||||
for (let p = 0; p < bodyParts.length; p++) {
|
||||
let sl = bodyParts[p].startLine;
|
||||
if (sl === undefined || sl < 0) continue;
|
||||
if (sl <= targetLine) pIdx = p; else break;
|
||||
}
|
||||
if (pIdx < 0)
|
||||
return 0;
|
||||
let item = bodyRepeater.itemAt(pIdx);
|
||||
if (!item)
|
||||
return 0;
|
||||
// y of the segment relative to this delegate.
|
||||
return item.mapToItem(del, 0, 0).y;
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: outline.currentIndex = del.index
|
||||
|
|
@ -489,6 +552,7 @@ ListView {
|
|||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
id: bodyRepeater
|
||||
model: del.bodyParts
|
||||
|
||||
delegate: Loader {
|
||||
|
|
|
|||
Loading…
Reference in New Issue