Support cross-file refile via spring-loaded drag
Dragging a section's grip can now drop it into another file: hover the drag over a document in the sidebar for ~450ms and the view springs to that file, after which the normal in-file drop chooses the exact position. The subtree is removed from the source file and inserted in the target (reparented, re-leveled, and placed before the chosen sibling). The key change is making the drag scene-level so it survives the view switching files mid-drag: Model (decouple source from view): - beginDrag(row) captures (sourceDoc, sourceId) by value; dragSourceDoc()/ dragSourceId() expose them for the drag payload. - resolveDrop(gapAboveRow, desiredLevel) resolves against the current view and excludes the dragged subtree only when it lives in the current file (so cross-file drags, whose source isn't present, exclude nothing). - commitDrag(parentId, beforeId) issues refile <sourceDoc> <id> <curDoc> … so it targets whatever file is now shown; endDrag() clears state. - Drops the old row-indexed refileSection/resolveDrop. GUI: - OutlineView: the grip starts a Qt Automatic (scene) drag on an invisible proxy carrying the section payload (keys ["ork-section"]); a DropArea over the outline resolves the live position and commits on drop. Because the drag is scene-level, springing the view no longer cancels it. - Main.qml: each sidebar document is a DropArea with a 450ms spring Timer that selects that document when a section is held over it; it highlights while a drag hovers. Verified: cross-file refile over 9P (refile src mover tgt two child) moves the subtree between files, re-levels it, inserts before the sibling, and carries its body. Server tests still green. GUI builds clean. The scene-drag gesture and spring timing can't be verified headlessly; the data path (begin/resolve/commit + cross-doc refile) is confirmed.
This commit is contained in:
parent
614378ffa4
commit
b329bcebd0
|
|
@ -165,6 +165,7 @@ ApplicationWindow {
|
|||
clip: true
|
||||
model: []
|
||||
delegate: ItemDelegate {
|
||||
id: docDelegate
|
||||
required property var modelData
|
||||
width: docList.width
|
||||
highlighted: root.currentDoc === modelData.name
|
||||
|
|
@ -174,8 +175,38 @@ ApplicationWindow {
|
|||
topPadding: root.baseFontSize * 0.4
|
||||
bottomPadding: root.baseFontSize * 0.4
|
||||
background: Rectangle {
|
||||
color: highlighted ? theme.highlightColor : "transparent"
|
||||
opacity: highlighted ? 0.3 : 1.0
|
||||
color: (docDrop.containsDrag) ? theme.highlightColor
|
||||
: (docDelegate.highlighted ? theme.highlightColor : "transparent")
|
||||
opacity: docDrop.containsDrag ? 0.5
|
||||
: (docDelegate.highlighted ? 0.3 : 1.0)
|
||||
// While a section is dragged over a non-current doc,
|
||||
// show a border that fills as the spring timer runs.
|
||||
border.color: theme.linkColor
|
||||
border.width: docDrop.containsDrag
|
||||
&& modelData.name !== root.currentDoc ? 1 : 0
|
||||
}
|
||||
|
||||
// Spring-loaded cross-file drop target: hovering a
|
||||
// dragged section over another document for a moment
|
||||
// switches the view to it; the drop then lands in that
|
||||
// file via the outline's normal in-file resolution.
|
||||
DropArea {
|
||||
id: docDrop
|
||||
anchors.fill: parent
|
||||
keys: ["ork-section"]
|
||||
onEntered: {
|
||||
if (docDelegate.modelData.name !== root.currentDoc)
|
||||
springTimer.restart()
|
||||
}
|
||||
onExited: springTimer.stop()
|
||||
Timer {
|
||||
id: springTimer
|
||||
interval: 450
|
||||
onTriggered: {
|
||||
if (docDrop.containsDrag)
|
||||
root.selectDoc(docDelegate.modelData.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
contentItem: ColumnLayout {
|
||||
spacing: root.baseFontSize * 0.15
|
||||
|
|
|
|||
|
|
@ -19,23 +19,23 @@ ListView {
|
|||
property var insertTarget: null
|
||||
|
||||
// ── Drag-and-drop structural move state ──────────────────────────────
|
||||
// The visible row currently being dragged (-1 = none), the live resolved
|
||||
// drop target from docModel.resolveDrop, and the gap geometry for drawing
|
||||
// the insertion indicator.
|
||||
property int dragRow: -1
|
||||
// A drag is scene-level (Qt Drag + DropArea) so the view can spring-load
|
||||
// to another file mid-drag without cancelling it. dragActive is true for
|
||||
// the duration; dropInfo is the live resolved target over the current
|
||||
// outline; the indicator geometry is in content coordinates.
|
||||
property bool dragActive: false
|
||||
property var dropInfo: null // {valid, level, parentId, parentTitle, beforeId}
|
||||
property real dropIndicatorY: 0 // content-y of the insertion bar
|
||||
property int dropGapAbove: -2 // visible row above the gap (-1 = top)
|
||||
property bool dropOverOutline: false // pointer currently over the outline
|
||||
|
||||
// Recompute the drop target from a pointer position in content coords.
|
||||
// The dragged section is tracked by the model (beginDrag), so resolveDrop
|
||||
// needs only the gap and desired level.
|
||||
function updateDrop(contentX, contentY) {
|
||||
if (dragRow < 0) return
|
||||
// Which row is the pointer over? The gap is above or below its middle.
|
||||
if (!dragActive) return
|
||||
var r = indexAt(1, contentY)
|
||||
if (r < 0) {
|
||||
// Past the last row → gap at the very bottom.
|
||||
if (r < 0)
|
||||
r = count - 1
|
||||
}
|
||||
var item = itemAtIndex(r)
|
||||
var gapAbove
|
||||
var gapY
|
||||
|
|
@ -46,24 +46,23 @@ ListView {
|
|||
} else {
|
||||
gapAbove = r; gapY = contentY
|
||||
}
|
||||
// Desired level from horizontal indent. Row left padding is
|
||||
// indent(level) + 8; one level ≈ indentWidth px.
|
||||
// Desired level from horizontal indent (row left padding = indent + 8).
|
||||
var desired = Math.round((contentX - 8) / outline.indentWidth) + 1
|
||||
if (desired < 1) desired = 1
|
||||
var info = docModel.resolveDrop(dragRow, gapAbove, desired)
|
||||
outline.dropInfo = info
|
||||
outline.dropGapAbove = gapAbove
|
||||
outline.dropInfo = docModel.resolveDrop(gapAbove, desired)
|
||||
outline.dropIndicatorY = gapY
|
||||
}
|
||||
|
||||
// Commit the current drop (called on drag release).
|
||||
// Commit the current drop into the current document (may be a different
|
||||
// file than the drag started in — the model holds the source).
|
||||
function commitDrop() {
|
||||
if (dragRow >= 0 && dropInfo && dropInfo.valid) {
|
||||
docModel.refileSection(dragRow, dropInfo.parentId, dropInfo.beforeId)
|
||||
}
|
||||
dragRow = -1
|
||||
if (dragActive && dropOverOutline && dropInfo && dropInfo.valid)
|
||||
docModel.commitDrag(dropInfo.parentId, dropInfo.beforeId)
|
||||
else
|
||||
docModel.endDrag()
|
||||
dragActive = false
|
||||
dropInfo = null
|
||||
dropGapAbove = -2
|
||||
dropOverOutline = false
|
||||
}
|
||||
|
||||
// Palette for TODO keywords by state.
|
||||
|
|
@ -184,6 +183,31 @@ ListView {
|
|||
}
|
||||
}
|
||||
|
||||
// Drop target covering the outline viewport. Receives the scene drag
|
||||
// (started from a row's grip) and resolves the in-file position live.
|
||||
// Because the drag is scene-level, this DropArea keeps working even after
|
||||
// the view springs to a different file mid-drag.
|
||||
DropArea {
|
||||
id: outlineDrop
|
||||
anchors.fill: parent
|
||||
z: 90
|
||||
keys: ["ork-section"]
|
||||
|
||||
onEntered: outline.dropOverOutline = true
|
||||
onExited: outline.dropOverOutline = false
|
||||
onPositionChanged: (drag) => {
|
||||
outline.dropOverOutline = true
|
||||
// DropArea-local coords are viewport coords; add contentY for the
|
||||
// content coordinate the row geometry uses.
|
||||
outline.updateDrop(drag.x, drag.y + outline.contentY)
|
||||
}
|
||||
onDropped: (drop) => {
|
||||
outline.dropOverOutline = true
|
||||
outline.commitDrop()
|
||||
drop.accept()
|
||||
}
|
||||
}
|
||||
|
||||
// Drop indicator shown while dragging a section. A horizontal insertion
|
||||
// bar at the resolved gap, indented to the resolved level, with a ghost
|
||||
// label naming the resolved level and parent. Driven entirely by
|
||||
|
|
@ -191,7 +215,8 @@ ListView {
|
|||
Item {
|
||||
id: dropIndicator
|
||||
z: 100
|
||||
visible: outline.dragRow >= 0 && outline.dropInfo && outline.dropInfo.valid
|
||||
visible: outline.dragActive && outline.dropOverOutline
|
||||
&& outline.dropInfo && outline.dropInfo.valid
|
||||
// Convert the content-y gap position to viewport coordinates.
|
||||
y: outline.dropIndicatorY - outline.contentY
|
||||
x: 0
|
||||
|
|
@ -517,21 +542,20 @@ ListView {
|
|||
Layout.fillWidth: true
|
||||
spacing: 8
|
||||
|
||||
// Drag grip (left gutter): a dedicated drag control. Pressing
|
||||
// it disables ListView flicking for the duration of the press,
|
||||
// so the grip — not the enclosing Flickable — owns the gesture;
|
||||
// dragging then restructures the section (and its subtree) and
|
||||
// releasing commits a refile. Horizontal position chooses the
|
||||
// depth, vertical the gap. Flicking is restored on release.
|
||||
// Drag grip (left gutter): starts a scene-level drag of this
|
||||
// section (and its subtree). The drag is a Qt Automatic drag so
|
||||
// it is delivered to DropAreas across the window and survives
|
||||
// the view springing to another file mid-drag. The outline's
|
||||
// DropArea resolves the in-file position; a sidebar doc's
|
||||
// DropArea spring-loads to that file.
|
||||
Rectangle {
|
||||
id: gripLabel
|
||||
Layout.preferredWidth: outline.baseFontSize + 8
|
||||
Layout.preferredHeight: outline.baseFontSize + 8
|
||||
radius: 3
|
||||
readonly property bool active: outline.dragRow === del.index
|
||||
|| gripPress.pressed
|
||||
readonly property bool active: gripMouse.pressed
|
||||
color: active ? theme.highlightColor
|
||||
: (gripHover.hovered ? theme.alternateBaseColor : "transparent")
|
||||
: (gripMouse.containsMouse ? theme.alternateBaseColor : "transparent")
|
||||
|
||||
Label {
|
||||
anchors.centerIn: parent
|
||||
|
|
@ -539,49 +563,51 @@ ListView {
|
|||
color: gripLabel.active ? theme.highlightedTextColor
|
||||
: theme.placeholderTextColor
|
||||
font.pointSize: outline.baseFontSize - 1
|
||||
opacity: gripHover.hovered || gripLabel.active ? 1.0 : 0.4
|
||||
opacity: gripMouse.containsMouse || gripLabel.active ? 1.0 : 0.4
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: gripHover
|
||||
cursorShape: gripLabel.active ? Qt.ClosedHandCursor : Qt.OpenHandCursor
|
||||
// Invisible proxy that carries the drag payload. Starting a
|
||||
// Qt Automatic drag on it hands the gesture to the window's
|
||||
// drag manager, independent of this (destructible) delegate.
|
||||
Item {
|
||||
id: dragProxy
|
||||
anchors.fill: parent
|
||||
Drag.active: false
|
||||
Drag.dragType: Drag.Automatic
|
||||
Drag.supportedActions: Qt.MoveAction
|
||||
Drag.keys: ["ork-section"]
|
||||
}
|
||||
|
||||
// Own the gesture: while the grip is pressed, the ListView
|
||||
// must not flick. This is what makes the gutter behave as a
|
||||
// separate control rather than fighting the Flickable.
|
||||
TapHandler {
|
||||
id: gripPress
|
||||
onPressedChanged: outline.interactive = !pressed
|
||||
}
|
||||
|
||||
// The structural drag. The Flickable is non-interactive
|
||||
// while pressed, so the handler takes the grab cleanly.
|
||||
DragHandler {
|
||||
id: gripDrag
|
||||
target: null // track only; don't move the item
|
||||
dragThreshold: 3
|
||||
grabPermissions: PointerHandler.CanTakeOverFromItems
|
||||
| PointerHandler.CanTakeOverFromHandlersOfDifferentType
|
||||
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
outline.dragRow = del.index
|
||||
} else {
|
||||
outline.commitDrop()
|
||||
outline.interactive = true // belt-and-suspenders
|
||||
MouseArea {
|
||||
id: gripMouse
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: gripLabel.active ? Qt.ClosedHandCursor
|
||||
: Qt.OpenHandCursor
|
||||
preventStealing: true
|
||||
property bool started: false
|
||||
onPressed: started = false
|
||||
onPositionChanged: (mouse) => {
|
||||
if (started) return
|
||||
started = true
|
||||
// Record the drag source in the model (survives a
|
||||
// later file switch) and begin the OS drag.
|
||||
docModel.beginDrag(del.index)
|
||||
outline.dragActive = true
|
||||
dragProxy.Drag.mimeData = {
|
||||
"text/plain": docModel.dragSourceDoc()
|
||||
+ "\t" + docModel.dragSourceId()
|
||||
}
|
||||
dragProxy.Drag.active = true
|
||||
dragProxy.Drag.startDrag()
|
||||
}
|
||||
onCentroidChanged: {
|
||||
if (!active || outline.dragRow < 0) return
|
||||
// Map the scene pointer position to ListView content
|
||||
// coordinates. mapFromItem(null, ...) treats the
|
||||
// source coordinates as scene coordinates.
|
||||
var p = outline.contentItem.mapFromItem(
|
||||
null,
|
||||
centroid.scenePosition.x,
|
||||
centroid.scenePosition.y)
|
||||
outline.updateDrop(p.x, p.y)
|
||||
onReleased: {
|
||||
// If the drag never produced a drop, clean up.
|
||||
if (outline.dragActive && !started) {
|
||||
outline.dragActive = false
|
||||
docModel.endDrag()
|
||||
}
|
||||
started = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -555,30 +555,6 @@ bool DocumentModel::demoteSection(int row)
|
|||
return true;
|
||||
}
|
||||
|
||||
bool DocumentModel::refileSection(int row, const QString &parentId, const QString &beforeId)
|
||||
{
|
||||
if (!m_client || m_document.isEmpty() || row < 0 || row >= m_visibleRows.size())
|
||||
return false;
|
||||
const QString id = m_visibleRows[row];
|
||||
QString doc = m_document;
|
||||
if (doc.startsWith('/'))
|
||||
doc = doc.mid(1);
|
||||
// refile <doc> <id> <to-doc> [parent-id] [before-id]; same-doc refile uses
|
||||
// doc as to-doc. A "-" placeholder keeps positional args when only
|
||||
// before-id is given (parent empty = top level).
|
||||
QString cmd = QStringLiteral("refile %1 %2 %1").arg(doc, id);
|
||||
const QString parent = parentId.isEmpty() ? QStringLiteral("-") : parentId;
|
||||
if (!beforeId.isEmpty())
|
||||
cmd += QStringLiteral(" %1 %2").arg(parent, beforeId);
|
||||
else if (!parentId.isEmpty())
|
||||
cmd += QStringLiteral(" %1").arg(parentId);
|
||||
const QString reply = m_client->rdwr(QStringLiteral("/ctl"), cmd);
|
||||
if (!reply.startsWith(QLatin1String("ok")))
|
||||
return false;
|
||||
reload();
|
||||
return true;
|
||||
}
|
||||
|
||||
QString DocumentModel::sectionIdAt(int row)
|
||||
{
|
||||
if (row < 0 || row >= m_visibleRows.size())
|
||||
|
|
@ -586,28 +562,85 @@ QString DocumentModel::sectionIdAt(int row)
|
|||
return m_visibleRows[row];
|
||||
}
|
||||
|
||||
QVariantMap DocumentModel::resolveDrop(int draggedRow, int gapAboveRow, int desiredLevel)
|
||||
QString DocumentModel::beginDrag(int row)
|
||||
{
|
||||
if (row < 0 || row >= m_visibleRows.size()) {
|
||||
m_dragSourceDoc.clear();
|
||||
m_dragSourceId.clear();
|
||||
return QString();
|
||||
}
|
||||
m_dragSourceId = m_visibleRows[row];
|
||||
m_dragSourceDoc = m_document;
|
||||
if (m_dragSourceDoc.startsWith('/'))
|
||||
m_dragSourceDoc = m_dragSourceDoc.mid(1);
|
||||
return m_dragSourceId;
|
||||
}
|
||||
|
||||
void DocumentModel::endDrag()
|
||||
{
|
||||
m_dragSourceDoc.clear();
|
||||
m_dragSourceId.clear();
|
||||
}
|
||||
|
||||
bool DocumentModel::commitDrag(const QString &parentId, const QString &beforeId)
|
||||
{
|
||||
if (!m_client || m_dragSourceDoc.isEmpty() || m_dragSourceId.isEmpty())
|
||||
return false;
|
||||
|
||||
QString toDoc = m_document;
|
||||
if (toDoc.startsWith('/'))
|
||||
toDoc = toDoc.mid(1);
|
||||
|
||||
// refile <src-doc> <id> <to-doc> [parent-id] [before-id]. A "-" in the
|
||||
// parent slot means top level (lets a before-id be given at top level).
|
||||
const QString parent = parentId.isEmpty() ? QStringLiteral("-") : parentId;
|
||||
QString cmd = QStringLiteral("refile %1 %2 %3")
|
||||
.arg(m_dragSourceDoc, m_dragSourceId, toDoc);
|
||||
if (!beforeId.isEmpty())
|
||||
cmd += QStringLiteral(" %1 %2").arg(parent, beforeId);
|
||||
else if (!parentId.isEmpty())
|
||||
cmd += QStringLiteral(" %1").arg(parentId);
|
||||
|
||||
const QString reply = m_client->rdwr(QStringLiteral("/ctl"), cmd);
|
||||
endDrag();
|
||||
if (!reply.startsWith(QLatin1String("ok")))
|
||||
return false;
|
||||
reload();
|
||||
return true;
|
||||
}
|
||||
|
||||
QVariantMap DocumentModel::resolveDrop(int gapAboveRow, int desiredLevel)
|
||||
{
|
||||
QVariantMap result;
|
||||
result[QStringLiteral("valid")] = false;
|
||||
if (draggedRow < 0 || draggedRow >= m_visibleRows.size())
|
||||
return result;
|
||||
|
||||
const QString draggedId = m_visibleRows[draggedRow];
|
||||
if (!m_sections.contains(draggedId))
|
||||
return result;
|
||||
// Determine whether the drag source lives in the current view. If so, its
|
||||
// subtree is excluded (can't drop into itself); for a cross-file drag the
|
||||
// source isn't present, so there's nothing to exclude.
|
||||
QString currentDoc = m_document;
|
||||
if (currentDoc.startsWith('/'))
|
||||
currentDoc = currentDoc.mid(1);
|
||||
const bool sameDoc = (m_dragSourceDoc == currentDoc)
|
||||
&& m_sections.contains(m_dragSourceId);
|
||||
|
||||
// The rows covered by the dragged subtree (contiguous in visible order,
|
||||
// from draggedRow until a row at level <= dragged level).
|
||||
const int dragLevel = m_sections[draggedId].level;
|
||||
int dragEnd = draggedRow + 1; // exclusive
|
||||
while (dragEnd < m_visibleRows.size()) {
|
||||
const QString rid = m_visibleRows[dragEnd];
|
||||
if (!m_sections.contains(rid)) break;
|
||||
if (m_sections[rid].level <= dragLevel) break;
|
||||
++dragEnd;
|
||||
int draggedRow = -1;
|
||||
int dragEnd = -1; // exclusive end of the dragged subtree in visible order
|
||||
if (sameDoc) {
|
||||
draggedRow = m_visibleRows.indexOf(m_dragSourceId);
|
||||
if (draggedRow >= 0) {
|
||||
const int dragLevel = m_sections[m_dragSourceId].level;
|
||||
dragEnd = draggedRow + 1;
|
||||
while (dragEnd < m_visibleRows.size()) {
|
||||
const QString rid = m_visibleRows[dragEnd];
|
||||
if (!m_sections.contains(rid)) break;
|
||||
if (m_sections[rid].level <= dragLevel) break;
|
||||
++dragEnd;
|
||||
}
|
||||
}
|
||||
}
|
||||
auto inDragged = [&](int r) { return r >= draggedRow && r < dragEnd; };
|
||||
auto inDragged = [&](int r) {
|
||||
return draggedRow >= 0 && r >= draggedRow && r < dragEnd;
|
||||
};
|
||||
|
||||
// Row A directly above the gap (skip the dragged subtree), and its level.
|
||||
int aRow = gapAboveRow;
|
||||
|
|
|
|||
|
|
@ -141,25 +141,38 @@ public:
|
|||
Q_INVOKABLE bool promoteSection(int row);
|
||||
Q_INVOKABLE bool demoteSection(int row);
|
||||
|
||||
// Refile the subtree at `row` under `parentId` (empty = top level),
|
||||
// inserted before `beforeId` (empty = append at the end of the parent's
|
||||
// children / end of document). Uses the /ctl refile verb.
|
||||
Q_INVOKABLE bool refileSection(int row, const QString &parentId,
|
||||
const QString &beforeId);
|
||||
|
||||
// The section id at a visible row (empty if unknown).
|
||||
Q_INVOKABLE QString sectionIdAt(int row);
|
||||
|
||||
// Resolve a drag-and-drop drop point to an authoritative target.
|
||||
// `gapAboveRow` is the visible row directly above the insertion gap (-1 =
|
||||
// the gap at the very top). `desiredLevel` is the heading level the user
|
||||
// is aiming at via horizontal indent. The result is clamped to the legal
|
||||
// range for that gap and returned as a map:
|
||||
// { valid, level, parentId, parentTitle, beforeId }
|
||||
// where parentId/beforeId are empty for top level / append. The dragged
|
||||
// row is excluded so a subtree cannot be dropped into itself.
|
||||
Q_INVOKABLE QVariantMap resolveDrop(int draggedRow, int gapAboveRow,
|
||||
int desiredLevel);
|
||||
// ── Drag-and-drop refile (scene-level, cross-file capable) ───────────
|
||||
// The drag source is captured independently of the currently-viewed
|
||||
// document so the view can "spring-load" to another file mid-drag without
|
||||
// losing the dragged subtree.
|
||||
|
||||
// Begin a drag: record (sourceDoc, sourceId) from the current view for the
|
||||
// section at `row`. Returns the source id ("" if the row is invalid).
|
||||
Q_INVOKABLE QString beginDrag(int row);
|
||||
|
||||
// The document and id captured by beginDrag (for the drag payload / UI).
|
||||
Q_INVOKABLE QString dragSourceDoc() const { return m_dragSourceDoc; }
|
||||
Q_INVOKABLE QString dragSourceId() const { return m_dragSourceId; }
|
||||
|
||||
// Resolve a drop point against the CURRENT view. `gapAboveRow` is the
|
||||
// visible row directly above the insertion gap (-1 = top). `desiredLevel`
|
||||
// is the heading level the pointer's indent aims at. Clamped to the gap's
|
||||
// legal range. Returns { valid, level, parentId, parentTitle, beforeId }.
|
||||
// The drag-source subtree is excluded only when it lives in the current
|
||||
// document (so a subtree can't be dropped into itself); for a cross-file
|
||||
// drag the source isn't present here, so nothing is excluded.
|
||||
Q_INVOKABLE QVariantMap resolveDrop(int gapAboveRow, int desiredLevel);
|
||||
|
||||
// Commit the active drag: refile (sourceDoc, sourceId) into the CURRENT
|
||||
// document under `parentId` (empty = top level), before `beforeId` (empty
|
||||
// = append). Clears the drag state. Returns true on success.
|
||||
Q_INVOKABLE bool commitDrag(const QString &parentId, const QString &beforeId);
|
||||
|
||||
// Abandon the active drag without changes.
|
||||
Q_INVOKABLE void endDrag();
|
||||
|
||||
// Reveal a section by id: expand all its ancestors so it becomes visible,
|
||||
// and return its visible row index (-1 if the id is unknown). Emits
|
||||
|
|
@ -250,6 +263,10 @@ private:
|
|||
// Visible row order (section ids)
|
||||
QStringList m_visibleRows;
|
||||
|
||||
// Active drag source (captured by beginDrag; survives view/doc switches).
|
||||
QString m_dragSourceDoc;
|
||||
QString m_dragSourceId;
|
||||
|
||||
// Supported src-block languages from `/ctl languages` (loaded once).
|
||||
mutable QStringList m_supportedLanguages;
|
||||
mutable bool m_languagesLoaded = false;
|
||||
|
|
|
|||
Loading…
Reference in New Issue