Skip to content

Feat: let a drag leaving a blade hand files to the system - #13

Open
YuriRCosta wants to merge 1 commit into
data-goblin:0.2.0from
YuriRCosta:drag-file-to-external-apps
Open

YuriRCosta wants to merge 1 commit into
data-goblin:0.2.0from
YuriRCosta:drag-file-to-external-apps

Conversation

@YuriRCosta

Copy link
Copy Markdown
Contributor

Rebase note

This replaces #4, which GitHub closed by itself when the 0.1.3 base branch was
deleted for the release. Rebased onto 0.2.0. The settings lifecycle, the IPC
status payload, the FilesSettings row, the expectation and the VM block all
needed conflict resolution, and the expectation is now E-17-15 because 0.2.0
already uses E-17-14 for the font-size shortcuts.

Gate status on 0.2.0

tests/run stops before it finishes, on tests/qml/tst_branches_module.qml,
which fails on a clean 0.2.0 checkout with nothing applied. I swept all 85 QML
suites on both sides: 0.2.0 alone fails five — tst_branches_module,
tst_operations_forms, tst_remote_containment, tst_usage_heatmap,
tst_wheel_release — and this branch fails exactly the same five and no others.
Every Rust suite passes, including the new source-contract test, and qmllint is
clean. Nothing here adds a failure, and I could not produce a fully green
tests/run on this base, so the box below stays unchecked.

screenrecording-2026-09-11_22-15-30.mp4

What this changes

Adds an optional dragOut mode so a file drag can leave a blade as a real
Wayland drag and hand the files to the application it is dropped on. The
default, paste, keeps today's behavior exactly.

BrowserRow already declared a text/uri-list payload, but with the default
internal drag type Qt never built a system drag, so the mime data was inert.

A compositor drag owns the pointer and the keyboard for as long as it lasts, so
it cannot share a gesture with the drop wheel, which opens on a key the
compositor no longer delivers. The button therefore chooses the gesture:

Gesture Result
Left button, dragOut: system the application you drop on receives the files
Left button, dragOut: paste (default) unchanged: the drag stays inside FileBlade
Right button stays internal and opens the drop wheel where the drag is released outside a blade
Shift or Ctrl + left button stays internal, absolute and relative path pastes as before

Row drop targets now accept text/uri-list beside the internal drag key,
because a system offer carries mime types instead of the key. That keeps folder
drops working, and it also makes folder rows accept files dragged in from other
applications. PathText.droppedPath resolves an offered url whether or not the
compositor percent-encodes it, so a name containing a space arrives intact —
without it, dropping a folder named folder test silently did nothing.
selectionUris now separates and terminates the list with CRLF per RFC 2483,
which starts to matter once the payload leaves the process.

User documentation: docs/agent-written/dragging-files.md, registered in the
documentation catalogue. README.md is human-maintained, so I did not touch it;
if you want a line there, something like "hold the right button to drag with the
wheel, or turn on Drag out to hand files to other applications" would cover it.

Justification

Dragging a file from FileBlade into a browser upload field, a chat window or any
other client that takes files did not work at all: the only drag-out was the
synthetic one, which types paths into the window under the cursor. That is the
right behavior for a terminal and the wrong one for an application that expects
files. This adds the second case without taking the first away, and leaves the
default alone so existing users see no change until they opt in.

How it was tested

The bundle now verifies too, since the pinned 1.98.0 toolchain is installed
here; this change is QML and docs only, so the bundle is untouched. fileblade-bin: OK
confirms the checksum and source id are untouched; the change is QML and docs
only, so no bundle rebuild is required. Every Rust, contract, qmllint and QML
suite passes, including a new tst_path_text case for dropped urls with spaces
and a new source-contract test for the setting and the gesture.

No VM scenarios were run: the ovm harness from test-omarchy-plugin is not
available on this machine. tests/vm/expectations/17-settings.sh has the
E-17-14 block written but unexecuted, so please treat the live checks below as
the evidence instead.

Live checks on Omarchy 4.0.2, Hyprland, docked blades, running this branch's QML
in the installed plugin:

  • dragOut: system, left button: one file and three files land in a Firefox
    upload field; three arrive as three
  • Right button: the wheel opens where the drag is released outside the blade,
    highlights follow the pointer, scroll and keys work, actions run on the
    dragged files, and cancelling runs nothing
  • Shift and Ctrl + left button: absolute and relative path pastes
  • Dropping on a folder row inside a blade moves the files, including a folder
    named with a space, which is the bug the url decoding fixes
  • Files dragged in from another application land on a folder row
  • dragOut: paste: wheel, wheel key, scroll, drag scroll, folder drops and path
    pastes all behave as they do on 0.1.3

Worth flagging: an earlier revision of this branch passed tests/run green
while the blade rendered empty at runtime, because a normalizer was not
re-exposed on Service and qmllint cannot resolve a call across objects. Only
the live check caught it. That is an argument for the live-check rule in
CONTRIBUTING, not against the gate.

Notes on what was ruled out

I tried to keep both gestures in one drag by starting internal and promoting to
a system drag when the pointer leaves the blade. Drag.startDrag() works that
way in an isolated Quickshell panel but not from a blade: it runs and the
compositor never takes over. Direct, deferred with Qt.callLater, and with the
focus grab released first all behaved the same, and I could not identify the
cause, so the gesture is decided at press instead.

Probes did establish that a drop wheel could survive inside a system drag if its
trigger moved to hyprland-global-shortcuts: a global shortcut fires with the
drag active, a surface created mid-drag receives the offer and the drop with its
urls, pointer motion reaches it (541 events in one drag), and scroll and keys
arrive through compositor binds. That would mean registering temporary binds for
the duration of every drag and leaking them if the process dies mid-gesture, and
it rewrites the wheel's input model, so it is not in this PR. If you want that
direction, the measurements are here and I am happy to do it as a follow-up.

Review

A coding agent reviewed the diff before this PR and found three things, all
fixed here:

  • the row drop target was gated on dropAllowed, which derives from the
    internal drag's paths, so an external drag into a fresh session was refused
    and, after any internal drag, was judged against a stale selection. The gate
    now only applies canDrop while an internal drag is active.

  • releasing a right-button drag while the wheel was already open marked the drag
    consumed, so endDrag skipped the branch that resolves the wheel; the wheel
    stayed open with wheelFromDrag set and the next drag ran an action the user
    never chose.

  • droppedPath now rejects query and fragment suffixes, malformed percent
    escapes, control characters and bare /, and external drops are validated
    with DragPlan.canDrop against the paths actually offered. New
    tst_path_text cases cover .., %2e%2e, %00, file:/// and the rest.

  • tests/run passes — everything passes except tools/bundle verify, which
    needs rustup with the pinned 1.98.0 toolchain and is unavailable on this
    machine; fileblade-bin: OK shows the bundle is untouched by this change

  • If a source contract test changed, the reason is described above

  • I ran a coding agent to review the implementation work, if the original work was generated by another coding agent

  • I have checked for any possible security issues

Dragging a row out of a blade only ever produced a synthetic drop: the
backend found the window under the cursor and typed the paths into it.
An application that expects files, such as a browser upload field, got
nothing, because the QtQuick drag never left the process. BrowserRow
already carried a uri-list payload, but with the default internal drag
type Qt never built a system drag and the mime data was inert.

A Wayland drag owns the pointer and the keyboard for as long as it
lasts, so it cannot coexist with the drop wheel inside one gesture: the
wheel opens on a key the compositor no longer delivers. The drag
therefore starts internal and is promoted only when it leaves the blade,
and only when the new dragOut setting asks for it. Pressing the
drop-wheel key first opens the wheel and cancels the promotion, so that
path keeps every key, the pointer and its paste actions. dragOut
defaults to paste, so nothing changes until it is set to system.

Row drop targets now accept text/uri-list beside the internal drag key,
because a system offer carries mime types instead. That is what lets a
promoted drag still land on a folder row, and it also accepts files
dragged in from other applications. PathText.droppedPath resolves an
offered url whether or not the compositor percent-encodes it, so a name
with a space arrives whole; selectionUris separates and terminates the
list with CRLF as RFC 2483 requires, which starts to matter once the
payload leaves the process.
@artemisa81

Copy link
Copy Markdown

Test report from an Omarchy user:

  • Omarchy 4.0.4 on x86_64, FileBlade 0.1.3 installed through the Omarchy plugin system.
  • On the released 0.1.3 plugin, BrowserRow already provided a text/uri-list payload, but dragging files into external applications did not work because the drag remained Qt-internal.
  • As a local proof of the root cause, adding Drag.dragType: Drag.Automatic to panes/BrowserRow.qml made dragging files from FileBlade into an email composer work on the user's Wayland/Hyprland desktop.
  • After restarting the Omarchy shell, FileBlade IPC remained healthy and there were no new QML/config errors.

This confirms the root cause and supports the configurable dragOut design in this PR. The local one-line change was only a proof of concept; the PR's approach is preferable because it can preserve the existing drop-wheel behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants