From ed652359402e6d9ab2f8ce114aa8bb4bc01f3454 Mon Sep 17 00:00:00 2001 From: Levi Neely Date: Mon, 5 Oct 2026 09:19:24 +0200 Subject: [PATCH] Fix field clearing and gate drag behind press-and-hold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 ///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. --- crates/ork-server/src/p9.rs | 9 ++++-- ui/qml/OutlineView.qml | 62 ++++++++++++++++++++++++++----------- 2 files changed, 51 insertions(+), 20 deletions(-) diff --git a/crates/ork-server/src/p9.rs b/crates/ork-server/src/p9.rs index 3b32bb2..499df7b 100644 --- a/crates/ork-server/src/p9.rs +++ b/crates/ork-server/src/p9.rs @@ -506,8 +506,13 @@ impl Server { let mut fids = self.fids.write().unwrap(); if let Some(f) = fids.remove(&fid) { - // Flush write buffer if not rdwr mode - if !f.write_buf.is_empty() && f.node.rdwr_fn.is_none() { + // Flush the write buffer on clunk for non-rdwr files. A writable + // 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) { return self.error_response(tag, &e.to_string()); } diff --git a/ui/qml/OutlineView.qml b/ui/qml/OutlineView.qml index 8cd4ed7..296a955 100644 --- a/ui/qml/OutlineView.qml +++ b/ui/qml/OutlineView.qml @@ -26,6 +26,10 @@ ListView { 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) + // 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. function updateDrop(contentX, contentY) { @@ -517,33 +521,54 @@ ListView { Layout.fillWidth: true spacing: 8 - // Drag grip: drag a section (and its subtree) to restructure. - // A live indicator shows the resolved drop target; releasing - // commits a refile. Horizontal position chooses the depth, - // vertical position chooses the gap. A DragHandler is used - // (not a MouseArea) so the enclosing ListView cannot steal the - // drag for flicking. - Label { + // Drag grip (left gutter): press-and-hold to arm, then drag to + // restructure the section (and its subtree). A live indicator + // shows the resolved drop target; releasing commits a refile. + // Horizontal position chooses the depth, vertical the gap. + // + // Gating the drag behind a hold resolves the conflict with the + // ListView (a Flickable): a quick press-drag scrolls the list; + // a hold arms the grip so its DragHandler takes the grab. + Rectangle { id: gripLabel - text: "⠿" - color: theme.placeholderTextColor - // A slightly larger hit target than the bare glyph. - Layout.preferredWidth: outline.baseFontSize + 6 - horizontalAlignment: Text.AlignHCenter - font.pointSize: outline.baseFontSize - 1 - opacity: gripHover.hovered || outline.dragRow === del.index ? 1.0 : 0.3 + Layout.preferredWidth: outline.baseFontSize + 8 + Layout.preferredHeight: outline.baseFontSize + 8 + radius: 3 + readonly property bool armed: outline.dragArmed === del.index + || outline.dragRow === del.index + color: armed ? theme.highlightColor + : (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 { 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 { 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 - // Take the grab and refuse to yield it to the ListView - // (a Flickable), which would otherwise steal the drag. grabPermissions: PointerHandler.CanTakeOverFromItems | PointerHandler.CanTakeOverFromHandlersOfDifferentType @@ -552,6 +577,7 @@ ListView { outline.dragRow = del.index } else { outline.commitDrop() + outline.dragArmed = -1 } } onCentroidChanged: {