mirror of
https://github.com/robbert-vdh/yabridge.git
synced 2026-05-07 03:50:11 +02:00
Spawn a thread to fake do our XDND polling
We cannot integrate this into our event loop like we planned, because Wine a) grabs the mouse pointer so we cannot do that, and b) blocks the GUI thread. So instead we will spawn our own thread and do polling based XDND. When Wine's tracker window gets destroyed, we know that the left mouse button has been released.
This commit is contained in:
@@ -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<xcb_generic_event_t> 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -19,11 +19,18 @@
|
||||
#include <atomic>
|
||||
#include <iostream>
|
||||
|
||||
#include "boost-fix.h"
|
||||
|
||||
#include <boost/container/small_vector.hpp>
|
||||
#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<std::string>& 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<xcb_generic_event_t> 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<xcb_query_pointer_reply_t> 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
|
||||
|
||||
+49
-10
@@ -27,6 +27,9 @@
|
||||
#pragma pop_macro("_WIN32")
|
||||
|
||||
#include <windows.h>
|
||||
#include <boost/container/small_vector.hpp>
|
||||
|
||||
#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<xcb_connection_t> 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<std::string>& 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<std::string, 4> 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;
|
||||
|
||||
Reference in New Issue
Block a user