Fix field clearing and gate drag behind press-and-hold

Two issues with the editing UI:

Clearing a keyword/priority did nothing. The "None" menu items write an
empty value to the field, which the client sends as an OTRUNC open plus a
zero-length Twrite. But the server flushed the virtfs write handler on
clunk only when the write buffer was non-empty, so the empty write — the
whole point of a clear — was dropped and the field kept its old value.
Flush on clunk whenever the fid was opened for writing (non-rdwr), not
just when bytes were buffered, so an empty write clears the field.
Verified live: an empty stdin write to /<doc>/<id>/keyword and /priority
now removes the keyword and priority from the headline.

Drag-to-refile fought the ListView. The grip's DragHandler and the
enclosing Flickable both wanted the press, so dragging usually scrolled
instead of moving the section. Gate the drag behind a press-and-hold: a
left-gutter grip (⠿ in a rounded cell) arms on a 0.35s long press
(TapHandler.onLongPressed), and the DragHandler is enabled only while
armed. A quick press-drag now scrolls the list; a hold then drag performs
the structural move. The grip highlights while armed/dragging and shows
an open-hand/size-all cursor.

GUI builds clean; server builds clean.
This commit is contained in:
Levi Neely 2026-10-05 09:19:24 +02:00
parent 0a06ca60bf
commit ed65235940
2 changed files with 51 additions and 20 deletions

View File

@ -506,8 +506,13 @@ impl Server {
let mut fids = self.fids.write().unwrap(); let mut fids = self.fids.write().unwrap();
if let Some(f) = fids.remove(&fid) { if let Some(f) = fids.remove(&fid) {
// Flush write buffer if not rdwr mode // Flush the write buffer on clunk for non-rdwr files. A writable
if !f.write_buf.is_empty() && f.node.rdwr_fn.is_none() { // open with an empty buffer is a legitimate "set to empty" (the
// client opened with OTRUNC and wrote nothing, e.g. clearing a
// field), so flush whenever the fid was opened for writing — not
// only when the buffer is non-empty.
let opened_for_write = f.open_mode.map(|m| m.is_writable()).unwrap_or(false);
if f.node.rdwr_fn.is_none() && (opened_for_write || !f.write_buf.is_empty()) {
if let Err(e) = f.node.write(&f.write_buf) { if let Err(e) = f.node.write(&f.write_buf) {
return self.error_response(tag, &e.to_string()); return self.error_response(tag, &e.to_string());
} }

View File

@ -26,6 +26,10 @@ ListView {
property var dropInfo: null // {valid, level, parentId, parentTitle, beforeId} property var dropInfo: null // {valid, level, parentId, parentTitle, beforeId}
property real dropIndicatorY: 0 // content-y of the insertion bar property real dropIndicatorY: 0 // content-y of the insertion bar
property int dropGapAbove: -2 // visible row above the gap (-1 = top) property int dropGapAbove: -2 // visible row above the gap (-1 = top)
// Row whose grip has been "armed" by a press-and-hold, so its DragHandler
// may start a structural drag. This gates the drag behind a hold so a
// quick press-drag still scrolls the ListView (Flickable) instead.
property int dragArmed: -1
// Recompute the drop target from a pointer position in content coords. // Recompute the drop target from a pointer position in content coords.
function updateDrop(contentX, contentY) { function updateDrop(contentX, contentY) {
@ -517,33 +521,54 @@ ListView {
Layout.fillWidth: true Layout.fillWidth: true
spacing: 8 spacing: 8
// Drag grip: drag a section (and its subtree) to restructure. // Drag grip (left gutter): press-and-hold to arm, then drag to
// A live indicator shows the resolved drop target; releasing // restructure the section (and its subtree). A live indicator
// commits a refile. Horizontal position chooses the depth, // shows the resolved drop target; releasing commits a refile.
// vertical position chooses the gap. A DragHandler is used // Horizontal position chooses the depth, vertical the gap.
// (not a MouseArea) so the enclosing ListView cannot steal the //
// drag for flicking. // Gating the drag behind a hold resolves the conflict with the
Label { // ListView (a Flickable): a quick press-drag scrolls the list;
// a hold arms the grip so its DragHandler takes the grab.
Rectangle {
id: gripLabel id: gripLabel
text: "⠿" Layout.preferredWidth: outline.baseFontSize + 8
color: theme.placeholderTextColor Layout.preferredHeight: outline.baseFontSize + 8
// A slightly larger hit target than the bare glyph. radius: 3
Layout.preferredWidth: outline.baseFontSize + 6 readonly property bool armed: outline.dragArmed === del.index
horizontalAlignment: Text.AlignHCenter || outline.dragRow === del.index
font.pointSize: outline.baseFontSize - 1 color: armed ? theme.highlightColor
opacity: gripHover.hovered || outline.dragRow === del.index ? 1.0 : 0.3 : (gripHover.hovered ? theme.alternateBaseColor : "transparent")
Label {
anchors.centerIn: parent
text: "⠿"
color: gripLabel.armed ? theme.highlightedTextColor
: theme.placeholderTextColor
font.pointSize: outline.baseFontSize - 1
opacity: gripHover.hovered || gripLabel.armed ? 1.0 : 0.4
}
HoverHandler { HoverHandler {
id: gripHover id: gripHover
cursorShape: Qt.SizeAllCursor cursorShape: gripLabel.armed ? Qt.SizeAllCursor : Qt.OpenHandCursor
} }
// Press-and-hold to arm the drag without moving.
TapHandler {
id: gripHold
longPressThreshold: 0.35 // seconds
onLongPressed: outline.dragArmed = del.index
// A plain tap (no hold) shouldn't arm anything.
onTapped: outline.dragArmed = -1
}
// The structural drag itself. Only grabs once armed, so the
// Flickable keeps scrolling for un-armed press-drags.
DragHandler { DragHandler {
id: gripDrag id: gripDrag
target: null // we track, we don't move the item target: null // track only; don't move the item
enabled: gripLabel.armed
dragThreshold: 3 dragThreshold: 3
// Take the grab and refuse to yield it to the ListView
// (a Flickable), which would otherwise steal the drag.
grabPermissions: PointerHandler.CanTakeOverFromItems grabPermissions: PointerHandler.CanTakeOverFromItems
| PointerHandler.CanTakeOverFromHandlersOfDifferentType | PointerHandler.CanTakeOverFromHandlersOfDifferentType
@ -552,6 +577,7 @@ ListView {
outline.dragRow = del.index outline.dragRow = del.index
} else { } else {
outline.commitDrop() outline.commitDrop()
outline.dragArmed = -1
} }
} }
onCentroidChanged: { onCentroidChanged: {