diff --git a/src/wine-host/editor.cpp b/src/wine-host/editor.cpp index dba8b10d..c86b817b 100644 --- a/src/wine-host/editor.cpp +++ b/src/wine-host/editor.cpp @@ -48,12 +48,6 @@ using namespace std::literals::chrono_literals; */ constexpr size_t idle_timer_id = 1337; -/** - * The most significant bit in an X11 event's response type is used to indicate - * the event source. - */ -constexpr uint8_t event_type_mask = 0b0111'1111; - /** * The name of the X11 property on the root window used to denote the active * window in EWMH compliant window managers. @@ -355,15 +349,11 @@ void Editor::handle_x11_events() const noexcept { // function calls involving it will fail. All functions called from // here should be able to handle that cleanly. try { - // Pump X11 events for handling XDND client messages from the - // drag-and-drop proxy - dnd_proxy_handle.handle_x11_events(); - std::unique_ptr generic_event; while (generic_event.reset(xcb_poll_for_event(x11_connection.get())), generic_event != nullptr) { const uint8_t event_type = - generic_event->response_type & event_type_mask; + generic_event->response_type & xcb_event_type_mask; switch (event_type) { // We're listening for `ConfigureNotify` events on the topmost // window before the root window, i.e. the window that's diff --git a/src/wine-host/editor.h b/src/wine-host/editor.h index bcc7c6b9..faa2d778 100644 --- a/src/wine-host/editor.h +++ b/src/wine-host/editor.h @@ -33,6 +33,12 @@ #include "utils.h" #include "xdnd-proxy.h" +/** + * The most significant bit in an X11 event's response type is used to indicate + * the event source. + */ +constexpr uint8_t xcb_event_type_mask = 0b0111'1111; + /** * The maximum number of Win32 messages to handle per message loop. This is * needed because otherwise some plugins can run into an infinite loop. I've diff --git a/src/wine-host/xdnd-proxy.cpp b/src/wine-host/xdnd-proxy.cpp index 631efb53..aa3f53f4 100644 --- a/src/wine-host/xdnd-proxy.cpp +++ b/src/wine-host/xdnd-proxy.cpp @@ -19,11 +19,18 @@ #include #include -#include "boost-fix.h" - -#include #include "editor.h" +// As defined in `editor.cpp` +#define THROW_X11_ERROR(error) \ + do { \ + if (error) { \ + free(error); \ + throw std::runtime_error("X11 error in " + \ + std::string(__PRETTY_FUNCTION__)); \ + } \ + } while (0) + /** * The window class name Wine uses for its `DoDragDrop()` tracker window. * @@ -135,10 +142,6 @@ WineXdndProxy::Handle::Handle(Handle&& o) noexcept : proxy(o.proxy) { instance_reference_count += 1; } -void WineXdndProxy::Handle::handle_x11_events() const noexcept { - proxy->handle_x11_events(); -} - WineXdndProxy::Handle WineXdndProxy::get_handle() { // See the `instance` global above for an explanation on what's going on // here. @@ -149,8 +152,36 @@ WineXdndProxy::Handle WineXdndProxy::get_handle() { return Handle(instance); } -void WineXdndProxy::handle_x11_events() const noexcept { - // TODO +void WineXdndProxy::begin_xdnd( + const boost::container::small_vector_base& file_paths, + HWND tracker_window) { + // When XDND starts, we need to start listening for mouse events so we can + // react when the mouse cursor hovers over a target that supports XDND. The + // actual file contents will be transferred over X11 selections. See the + // spec for a description of the entire process: + // https://www.freedesktop.org/wiki/Specifications/XDND/#atomsandproperties + xcb_set_selection_owner(x11_connection.get(), proxy_window.window, + xcb_xdnd_selection, XCB_CURRENT_TIME); + xcb_flush(x11_connection.get()); + + // Normally at this point you would grab the mouse pointer and track what + // windows it's moving over. Wine is already doing this, so as a hacky + // workaround we will instead just periodically poll the pointer position in + // `WineXdndProxy::handle_x11_events()`, and we'll consider the + // disappearance of `tracker_window` to indicate that the drag-and-drop has + // either been cancelled or it has succeeded. + dragged_file_paths.assign(file_paths.begin(), file_paths.end()); + this->tracker_window = tracker_window; + + // Because Wine is blocking the GUI thread, we need to do our XDND polling + // from another thread. Luckily the X11 API is thread safe. + xdnd_handler = Win32Thread([&]() { run_xdnd_loop(); }); +} + +void WineXdndProxy::end_xdnd() { + xcb_set_selection_owner(x11_connection.get(), XCB_NONE, xcb_xdnd_selection, + XCB_CURRENT_TIME); + xcb_flush(x11_connection.get()); } /** @@ -166,6 +197,62 @@ struct TrackerWindowInfo { // ... more fields that we don't need }; +void WineXdndProxy::run_xdnd_loop() { + const xcb_window_t root_window = + xcb_setup_roots_iterator(xcb_get_setup(x11_connection.get())) + .data->root; + + // We cannot just grab the pointer because Wine is already doing that, and + // it's also blocking the GUI thread. So instead we will periodically poll + // the mouse cursor position, and we will consider the disappearance of + // `tracker_window` to mean that the drag-and-drop operation has ended. + uint16_t last_pointer_x = ~0; + uint16_t last_pointer_y = ~0; + while (IsWindow(tracker_window)) { + usleep(1000); + + // TODO: Can we somehow ignore plugin windows? + std::unique_ptr generic_event; + while (generic_event.reset(xcb_poll_for_event(x11_connection.get())), + generic_event != nullptr) { + const uint8_t event_type = + generic_event->response_type & xcb_event_type_mask; + switch (event_type) { + // TODO: Handle ConvertSelection + // TODO: Handle client messages + } + } + + xcb_generic_error_t* error = nullptr; + const xcb_query_pointer_cookie_t query_pointer_cookie = + xcb_query_pointer(x11_connection.get(), root_window); + const std::unique_ptr query_pointer_reply( + xcb_query_pointer_reply(x11_connection.get(), query_pointer_cookie, + &error)); + if (error) { + free(error); + continue; + } + + if (query_pointer_reply->root_x == last_pointer_x && + query_pointer_reply->root_y == last_pointer_y) { + continue; + } + + // TODO: Fetch the window under the mouse cursor, send messages to it + // according to the XDND protocol + + last_pointer_x = query_pointer_reply->root_x; + last_pointer_y = query_pointer_reply->root_y; + } + + // TODO: Check if the escape key is pressed to allow cancelling the drop, + // and either send the drop or leave message to the window that was + // under the pointer + + end_xdnd(); +} + void CALLBACK dnd_winevent_callback(HWINEVENTHOOK /*hWinEventHook*/, DWORD event, HWND hwnd, @@ -288,4 +375,13 @@ void CALLBACK dnd_winevent_callback(HWINEVENTHOOK /*hWinEventHook*/, << std::endl; return; } + + try { + instance->begin_xdnd(dragged_files, hwnd); + } catch (const std::exception& error) { + std::cerr << "XDND initialization failed:" << std::endl; + std::cerr << error.what() << std::endl; + } } + +#undef THROW_X11_ERROR diff --git a/src/wine-host/xdnd-proxy.h b/src/wine-host/xdnd-proxy.h index 47197dc7..9bf446f6 100644 --- a/src/wine-host/xdnd-proxy.h +++ b/src/wine-host/xdnd-proxy.h @@ -27,6 +27,9 @@ #pragma pop_macro("_WIN32") #include +#include + +#include "utils.h" /** * A simple, unmapped 1x1 proxy window we'll use for our Wine->X11 drag-and-drop @@ -52,8 +55,11 @@ class ProxyWindow { private: std::shared_ptr x11_connection; + + public: xcb_window_t window; + private: bool is_moved = false; }; @@ -99,11 +105,6 @@ class WineXdndProxy { Handle(Handle&&) noexcept; Handle& operator=(Handle&&) noexcept = default; - /** - * Handle X11 events for receiving XDND client messages. - */ - void handle_x11_events() const noexcept; - private: WineXdndProxy* proxy; @@ -122,9 +123,6 @@ class WineXdndProxy { * in a COM object, we can only handle drag-and-drop coming form this * process. * - * The handle's `handle_x11_events()` method should be periodically called - * to pump the X11 events for handling XDND client messages. - * * This is sort of a singleton but not quite, as the `WineXdndProxy` is only * alive for as long as there are open editors in this process. This is done * to avoid opening too many X11 connections. @@ -135,11 +133,28 @@ class WineXdndProxy { static WineXdndProxy::Handle get_handle(); /** - * Handle X11 events for receiving XDND client messages. + * Initiate the XDDN protocol by taking ownership of the `XdndSelection` + * selection and setting up the event listeners. */ - void handle_x11_events() const noexcept; + void begin_xdnd( + const boost::container::small_vector_base& file_paths, + HWND tracker_window); + + /** + * Release ownership of the selection stop listening for X11 events. + */ + void end_xdnd(); private: + /** + * From another thread, constantly poll the mouse position until + * `tracker_window` disappears, and then perform the drop if the mouse + * cursor was last positioned over an XDND aware window. This is a + * workaround for us not being able to grab the mouse cursor since Wine is + * already doing that. + */ + void run_xdnd_loop(); + /** * We need a dedicated X11 connection for our proxy because we can have * multiple open editors in a single process (e.g. when using VST3 plugins @@ -162,6 +177,30 @@ class WineXdndProxy { hook_handle; #pragma GCC diagnostic pop + /** + * The files that are currently being dragged. + */ + boost::container::small_vector dragged_file_paths; + + /** + * Wine's tracker window for tracking the drag-and-drop operation. Normally + * you would grab the mouse pointer when the drag-and-drop operation starts + * so you can track what windows you are hovering over, but we cannot do + * that because Wine is already doing just that. So instead we will + * periodically poll the mouse position from another thread, and we'll + * consider the disappearance of this window to mean that the drop has + * either succeeded or cancelled (depending on whether or not Escape is + * pressed). + */ + HWND tracker_window; + + /** + * We need to poll for mouse position changes from another thread, because + * when the drag-and-drop operation starts Wine will be blocking the GUI + * thread, so we cannot rely on the normal event loop. + */ + Win32Thread xdnd_handler; + // These are the atoms used for the XDND protocol, as described by // https://www.freedesktop.org/wiki/Specifications/XDND/#atomsandproperties xcb_atom_t xcb_xdnd_selection;