// yabridge: a Wine VST bridge
// Copyright (C) 2020-2021 Robbert van der Helm
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see .
#include "xdnd-proxy.h"
#include
#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.
*
* https://github.com/wine-mirror/wine/blob/d10887b8f56792ebcca717ccc28a289f7bcaf107/dlls/ole32/ole2.c#L101-L104
*/
constexpr char OLEDD_DRAGTRACKERCLASS[] = "WineDragDropTracker32";
// These are the XDND atom names as described in
// https://www.freedesktop.org/wiki/Specifications/XDND/#atomsandproperties
constexpr char xdnd_selection_name[] = "XdndSelection";
constexpr char xdnd_aware_property_name[] = "XdndAware";
constexpr char xdnd_proxy_property_name[] = "XdndProxy";
/**
* We're doing a bit of a hybrid between a COM-style reference counted smart
* pointer and a singleton here because we need to ensure that there's only one
* proxy per process, but we want to free up the X11 connection when it's not
* needed anymore. Because of that this pointer may point to deallocated memory,
* so the reference count should be leading here. Oh and explained elsewhere, we
* won't even bother making this thread safe because it can only be called from
* the GUI thread anyways.
*/
static WineXdndProxy* instance = nullptr;
/**
* The number of handles to our Wine->X11 drag-and-drop proxy object. To prevent
* running out of X11 connections when opening and closing a lot of plugin
* editors in a project, we'll free this again after the last editor in this
* process gets closed.
*/
static std::atomic_size_t instance_reference_count = 0;
void CALLBACK dnd_winevent_callback(HWINEVENTHOOK hWinEventHook,
DWORD event,
HWND hwnd,
LONG idObject,
LONG idChild,
DWORD idEventThread,
DWORD dwmsEventTime);
ProxyWindow::ProxyWindow(std::shared_ptr x11_connection)
: x11_connection(x11_connection),
window(xcb_generate_id(x11_connection.get())) {
const xcb_screen_t* screen =
xcb_setup_roots_iterator(xcb_get_setup(x11_connection.get())).data;
xcb_create_window(x11_connection.get(), XCB_COPY_FROM_PARENT, window,
screen->root, 0, 0, 1, 1, 0, XCB_WINDOW_CLASS_INPUT_ONLY,
XCB_COPY_FROM_PARENT, 0, nullptr);
xcb_flush(x11_connection.get());
}
ProxyWindow::~ProxyWindow() noexcept {
if (!is_moved) {
xcb_destroy_window(x11_connection.get(), window);
xcb_flush(x11_connection.get());
}
}
ProxyWindow::ProxyWindow(ProxyWindow&& o) noexcept
: x11_connection(std::move(o.x11_connection)), window(std::move(o.window)) {
o.is_moved = true;
}
ProxyWindow& ProxyWindow::operator=(ProxyWindow&& o) noexcept {
if (&o != this) {
x11_connection = std::move(o.x11_connection);
window = std::move(o.window);
o.is_moved = true;
}
return *this;
}
WineXdndProxy::WineXdndProxy()
: x11_connection(xcb_connect(nullptr, nullptr), xcb_disconnect),
proxy_window(x11_connection),
hook_handle(
SetWinEventHook(EVENT_OBJECT_CREATE,
EVENT_OBJECT_CREATE,
nullptr,
dnd_winevent_callback,
0,
0,
WINEVENT_OUTOFCONTEXT | WINEVENT_SKIPOWNPROCESS),
UnhookWinEvent) {
// XDND uses a whole load of atoms for its messages, properties, and
// selections
xcb_xdnd_selection = get_atom_by_name(*x11_connection, xdnd_selection_name);
xcb_xdnd_aware_property =
get_atom_by_name(*x11_connection, xdnd_aware_property_name);
xcb_xdnd_proxy_property =
get_atom_by_name(*x11_connection, xdnd_proxy_property_name);
}
WineXdndProxy::Handle::Handle(WineXdndProxy* proxy) : proxy(proxy) {}
WineXdndProxy::Handle::~Handle() noexcept {
if (instance_reference_count.fetch_sub(1) == 1) {
delete proxy;
}
}
WineXdndProxy::Handle::Handle(const Handle& o) noexcept : proxy(o.proxy) {
instance_reference_count += 1;
}
WineXdndProxy::Handle::Handle(Handle&& o) noexcept : proxy(o.proxy) {
instance_reference_count += 1;
}
WineXdndProxy::Handle WineXdndProxy::get_handle() {
// See the `instance` global above for an explanation on what's going on
// here.
if (instance_reference_count.fetch_add(1) == 0) {
instance = new WineXdndProxy{};
}
return Handle(instance);
}
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());
}
/**
* Part of the struct Wine uses to keep track of the data during an OLE
* drag-and-drop operation. We only really care about the first field that
* contains the actual data.
*
* https://github.com/wine-mirror/wine/blob/d10887b8f56792ebcca717ccc28a289f7bcaf107/dlls/ole32/ole2.c#L54-L73
*/
struct TrackerWindowInfo {
IDataObject* dataObject;
IDropSource* dropSource;
// ... 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,
LONG idObject,
LONG /*idChild*/,
DWORD /*idEventThread*/,
DWORD /*dwmsEventTime*/) {
if (!(event == EVENT_OBJECT_CREATE && idObject == OBJID_WINDOW)) {
return;
}
// Don't handle windows that weren't created in this process, because
// otherwise we obviously cannot access the `IDataObject` object
uint32_t process_id = 0;
GetWindowThreadProcessId(hwnd, &process_id);
if (process_id != GetCurrentProcessId()) {
return;
}
// Wine's drag-and-drop tracker windows always have the same window
// class name, so we can easily identify them
{
std::array window_class_name{0};
GetClassName(hwnd, window_class_name.data(), window_class_name.size());
if (strcmp(window_class_name.data(), OLEDD_DRAGTRACKERCLASS) != 0) {
return;
}
}
// They apaprently use 0 instead of `GWLP_USERDATA` to store the tracker
// data
auto tracker_info =
reinterpret_cast(GetWindowLongPtr(hwnd, 0));
if (!tracker_info || !tracker_info->dataObject) {
return;
}
IEnumFORMATETC* enumerator = nullptr;
tracker_info->dataObject->EnumFormatEtc(DATADIR_GET, &enumerator);
if (!enumerator) {
return;
}
// The plugin will indicate which formats they support for the
// drag-and-drop. In practice this is always going to be a single `HDROP`
// (through some `HGLOBAL` global memory) that contains a single file path.
// With this information we will set up XDND with those file paths, so we
// can drop the files onto native applications.
std::array supported_formats{};
unsigned int num_formats = 0;
enumerator->Next(supported_formats.size(), supported_formats.data(),
&num_formats);
enumerator->Release();
// This will contain the normal, Unix-style paths to the files
boost::container::small_vector dragged_files;
for (unsigned int format_idx = 0; format_idx < num_formats; format_idx++) {
STGMEDIUM storage{};
if (HRESULT result = tracker_info->dataObject->GetData(
&supported_formats[format_idx], &storage);
result == S_OK) {
switch (storage.tymed) {
case TYMED_HGLOBAL: {
auto drop = static_cast(GlobalLock(storage.hGlobal));
if (!drop) {
std::cerr << "Failed to lock global memory in "
"drag-and-drop operation"
<< std::endl;
continue;
}
std::array file_name{0};
const uint32_t num_files = DragQueryFileW(
drop, 0xFFFFFFFF, file_name.data(), file_name.size());
for (uint32_t file_idx = 0; file_idx < num_files;
file_idx++) {
file_name[0] = 0;
DragQueryFileW(drop, file_idx, file_name.data(),
file_name.size());
dragged_files.emplace_back(
wine_get_unix_file_name(file_name.data()));
}
GlobalUnlock(storage.hGlobal);
} break;
case TYMED_FILE: {
dragged_files.emplace_back(
wine_get_unix_file_name(storage.lpszFileName));
} break;
default: {
std::cerr << "Unknown drag-and-drop format "
<< storage.tymed << std::endl;
} break;
}
if (storage.pUnkForRelease) {
storage.pUnkForRelease->Release();
}
}
}
if (dragged_files.empty()) {
std::cerr
<< "Plugin wanted to drag-and-drop, but didn't specify any files"
<< std::endl;
return;
}
std::cerr << "Plugin wanted to drag-and-drop " << dragged_files.size()
<< (dragged_files.size() == 1 ? " file:" : " files:")
<< std::endl;
for (const auto& file : dragged_files) {
std::cerr << "- " << file << std::endl;
}
// This shouldn't be possible, but you can never be too sure!
if (instance_reference_count <= 0 || !instance) {
std::cerr << "Drag-and-drop proxy has not yet been initialized"
<< 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