// 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 #include "editor.h" using namespace std::literals::chrono_literals; namespace fs = boost::filesystem; // 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"; // xdnd_aware_property_name is defined in `editor.h`` constexpr char xdnd_proxy_property_name[] = "XdndProxy"; constexpr char xdnd_drop_message_name[] = "XdndDrop"; constexpr char xdnd_enter_message_name[] = "XdndEnter"; constexpr char xdnd_finished_message_name[] = "XdndFinished"; constexpr char xdnd_position_message_name[] = "XdndPosition"; constexpr char xdnd_status_message_name[] = "XdndStatus"; constexpr char xdnd_leave_message_name[] = "XdndLeave"; // XDND actions constexpr char xdnd_copy_action_name[] = "XdndActionCopy"; // Mime types for use in XDND constexpr char mime_text_uri_list_name[] = "text/uri-list"; constexpr char mime_text_plain_name[] = "text/plain"; // We can cheat by just using the Win32 cursors instead of providing our own static const HCURSOR dnd_accepted_cursor = LoadCursor(nullptr, IDC_HAND); static const HCURSOR dnd_denied_cursor = LoadCursor(nullptr, IDC_NO); /** * 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); xcb_xdnd_drop_message = get_atom_by_name(*x11_connection, xdnd_drop_message_name); xcb_xdnd_enter_message = get_atom_by_name(*x11_connection, xdnd_enter_message_name); xcb_xdnd_finished_message = get_atom_by_name(*x11_connection, xdnd_finished_message_name); xcb_xdnd_position_message = get_atom_by_name(*x11_connection, xdnd_position_message_name); xcb_xdnd_status_message = get_atom_by_name(*x11_connection, xdnd_status_message_name); xcb_xdnd_leave_message = get_atom_by_name(*x11_connection, xdnd_leave_message_name); xcb_xdnd_copy_action = get_atom_by_name(*x11_connection, xdnd_copy_action_name); xcb_mime_text_uri_list = get_atom_by_name(*x11_connection, mime_text_uri_list_name); xcb_mime_text_plain = get_atom_by_name(*x11_connection, mime_text_plain_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) { if (file_paths.empty()) { throw std::runtime_error("Cannot drag-and-drop without any files"); } // 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()); // We will transfer the files in `text/uri-list` format, so a string of URIs // separated by line feeds. When the target window requests the selection to // be converted, they will ask us to write this to a property on their // window constexpr char file_protocol[] = "file://"; dragged_files_uri_list.clear(); dragged_files_uri_list.reserve(std::accumulate( file_paths.begin(), file_paths.end(), 0, [](size_t size, const auto& path) { // Account for the protocol, the trailing line feed, and URL // encoding return size + static_cast(static_cast(path.size()) * 1.2); })); for (const auto& path : file_paths) { dragged_files_uri_list.append(file_protocol); dragged_files_uri_list.append(url_encode_path(path.string())); dragged_files_uri_list.push_back('\n'); } // 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 just poll the mouse position every millisecond until // the left mouse button gets released. Because Wine is also 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()); } // FIXME: For some reason you get a -Wmaybe-uninitialized false positive with // GCC 11.1.0 if you just dereference `last_window` here: // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=80635 // // Oh and Clang doesn't know about -Wmaybe-uninitialized, so we need to // ignore some more warnings here to get clangd to not complain #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpragmas" #pragma GCC diagnostic ignored "-Wunknown-warning-option" #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" void WineXdndProxy::run_xdnd_loop() { const xcb_window_t root_window = xcb_setup_roots_iterator(xcb_get_setup(x11_connection.get())) .data->root; const HWND windows_desktop_window = GetDesktopWindow(); std::optional last_xdnd_window; // Position and status messages should be sent in lockstep, which makes // everything a bit more complicated. Because of that we may need to spool // the `XdndPosition` messages. This field stores the next position we // should send to `last_xdnd_window` (i.e. `(root_x << 16) | root_y`). We // won't need to spool anything when `last_window_accepted_status` contains // a value. std::optional next_position_message_position; // We need to wait until we receive the last `XdndStatus` message until we // send a leave, finished, or another position message bool last_window_accepted_status = false; bool waiting_for_status_message = false; auto maybe_leave_last_window = [&]() { if (last_xdnd_window) { send_xdnd_message(*last_xdnd_window, xcb_xdnd_leave_message, 0, 0, 0, 0); next_position_message_position.reset(); last_window_accepted_status = false; waiting_for_status_message = false; } }; auto maybe_send_spooled_status_message = [&]() { if (next_position_message_position && !waiting_for_status_message) { send_xdnd_message(*last_xdnd_window, xcb_xdnd_position_message, 0, *next_position_message_position, XCB_CURRENT_TIME, xcb_xdnd_copy_action); next_position_message_position.reset(); waiting_for_status_message = true; } }; // 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 end the drag once the left mouse // button gets released. bool left_mouse_button_held = true; std::optional last_pointer_x; std::optional last_pointer_y; while (left_mouse_button_held) { usleep(1000); 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) { case XCB_SELECTION_REQUEST: { handle_convert_selection( *reinterpret_cast( generic_event.get())); } break; case XCB_CLIENT_MESSAGE: { const auto event = reinterpret_cast( generic_event.get()); if (event->type == xcb_xdnd_status_message) { const bool accepts_drop = static_cast(event->data.data32[1] & 0b01); // Because this is a Winelib we can cheat a bit here so // we don't have to create our own cursors. This will // probably also look better anyways. // XXX: Because Wine is also changing the cursor to a // denied symbol at the same time this looks a bit // off. Would it be better to just not do anything // at all here? if (accepts_drop) { SetCursor(dnd_accepted_cursor); } else { SetCursor(dnd_denied_cursor); } last_window_accepted_status = accepts_drop; waiting_for_status_message = false; } } break; } } // As explained above, we may need to spool these position messages // because they can only be sent again after we receive an `XdndStatus` // reply maybe_send_spooled_status_message(); // We'll try to find the first window under the pointer (starting form // the root) until we find a window that supports XDND. The returned // child window may not support XDND so we need to check that // separately, as we still need to keep track of the pointer // coordinates. const std::unique_ptr xdnd_window_query = query_xdnd_aware_window_at_pointer(root_window); if (!xdnd_window_query) { continue; } // We will stop the dragging operation as soon as the left mouse button // gets released // NOTE: In soem cases Wine's own drag-and-drop operation ends // prematurely. This seems to often happen with JUCE plugins. We // will still continue with the dragging operation, although at // that point the mouse pointer isn't grabbed by anything anymore. left_mouse_button_held = xdnd_window_query->mask & XCB_BUTTON_MASK_1; if (xdnd_window_query->root_x == last_pointer_x && xdnd_window_query->root_y == last_pointer_y) { continue; } last_pointer_x = xdnd_window_query->root_x; last_pointer_y = xdnd_window_query->root_y; if (!is_xdnd_aware(xdnd_window_query->child)) { maybe_leave_last_window(); last_xdnd_window.reset(); continue; } // We want to ignore all Wine windows (within this prefix), since Wine // will be able to handle the drag-and-drop better than we can POINT windows_pointer_pos; GetCursorPos(&windows_pointer_pos); if (HWND windows_window = WindowFromPoint(windows_pointer_pos); windows_window && windows_window != windows_desktop_window) { maybe_leave_last_window(); last_xdnd_window.reset(); continue; } // When transitioning between windows we need to announce this to // both windows if (last_xdnd_window != xdnd_window_query->child) { maybe_leave_last_window(); // We need to announce which file formats we support. There are a // couple more common ones, but with `text/uri-list` and // `text/plain` we should cover most applications, and this is also // the recommended format for links/paths elsewhere: // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#link send_xdnd_message(xdnd_window_query->child, xcb_xdnd_enter_message, 5 << 24, xcb_mime_text_uri_list, xcb_mime_text_plain, XCB_NONE); } // When the pointer is being moved inside of a window, we should // continuously send `XdndPosition` messages to that window. If the // window has not yet sent an `XdndStatus` reply to our last // `XdndPosition` message, then we need to spool this message and try // again on the next iteration. if (last_xdnd_window) { // XXX: We'll always stick with the copy action for now because that // seems safer than allowing the host to move the file // TODO: We should technically wait until we have received an // `XdndStatus` message const uint32_t position = (xdnd_window_query->root_x << 16) | xdnd_window_query->root_y; if (!waiting_for_status_message) { send_xdnd_message(xdnd_window_query->child, xcb_xdnd_position_message, 0, position, XCB_CURRENT_TIME, xcb_xdnd_copy_action); waiting_for_status_message = true; } else { next_position_message_position = position; } } // For efficiency's sake we'll only flush all of the client messages // we're sending once at the end of every cycle xcb_flush(x11_connection.get()); last_xdnd_window = xdnd_window_query->child; } // After the loop has finished we either: // 1) Finish the drop, if `last_xdnd_window` is a valid XDND window // 2) Cancel the drop, if the escape key is being held, or // 3) Don't do antyhing, if `last_xdnd_window` is a nullopt // TODO: Check if the escape key is pressed to allow cancelling the drop. In // that case we should call `maybe_leave_last_window()` if (!last_xdnd_window) { end_xdnd(); return; } // After the left mouse button has been released we will try to send the // drop to the last window we hovered over, if it was a valid XDND aware // window. We should however wait with this until the window has accepted // our `XdndPosition` message with an `XdndStatus` bool drop_finished = false; const std::chrono::steady_clock::time_point wait_start = std::chrono::steady_clock::now(); while (!drop_finished) { // In case that window somehow becomes unresponsive or disappears, we // will set a timeout here to avoid hanging if (std::chrono::steady_clock::now() - wait_start > 5s) { maybe_leave_last_window(); break; } usleep(1000); 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) { case XCB_SELECTION_REQUEST: { handle_convert_selection( *reinterpret_cast( generic_event.get())); } break; case XCB_CLIENT_MESSAGE: { const auto event = reinterpret_cast( generic_event.get()); if (event->type == xcb_xdnd_status_message) { // We may have to wait for the last `XdndStatus` to be // sent by the target window const bool accepts_drop = static_cast(event->data.data32[1] & 0b01); last_window_accepted_status = accepts_drop; waiting_for_status_message = false; } else if (event->type == xcb_xdnd_finished_message) { // At this point we're done here, and we can clean up // and terminate this thread drop_finished = true; } } break; } } // We May very well still have one unsent position change left maybe_send_spooled_status_message(); if (!waiting_for_status_message) { // After we receive the last `XdndStatus` message we'll know // whether the window accepts or denies the drop if (last_window_accepted_status) { send_xdnd_message(*last_xdnd_window, xcb_xdnd_drop_message, 0, XCB_CURRENT_TIME, 0, 0); } else { maybe_leave_last_window(); drop_finished = true; } xcb_flush(x11_connection.get()); // We obviously don't want to spam the other client waiting_for_status_message = true; } } end_xdnd(); } #pragma GCC diagnostic pop std::unique_ptr WineXdndProxy::query_xdnd_aware_window_at_pointer( xcb_window_t window) const noexcept { xcb_generic_error_t* error = nullptr; xcb_query_pointer_cookie_t query_pointer_cookie; std::unique_ptr query_pointer_reply = nullptr; while (true) { query_pointer_cookie = xcb_query_pointer(x11_connection.get(), window); query_pointer_reply.reset(xcb_query_pointer_reply( x11_connection.get(), query_pointer_cookie, &error)); if (error) { free(error); break; } // We want to find the first XDND aware window under the mouse pointer, // if there is any if (query_pointer_reply->child == XCB_NONE || is_xdnd_aware(query_pointer_reply->child)) { break; } window = query_pointer_reply->child; } return query_pointer_reply; } bool WineXdndProxy::is_xdnd_aware(xcb_window_t window) const noexcept { // Respect `XdndProxy`, if that's set window = get_xdnd_proxy(window).value_or(window); xcb_generic_error_t* error = nullptr; const xcb_get_property_cookie_t property_cookie = xcb_get_property(x11_connection.get(), false, window, xcb_xdnd_aware_property, XCB_ATOM_ATOM, 0, 1); const std::unique_ptr property_reply( xcb_get_property_reply(x11_connection.get(), property_cookie, &error)); if (error) { free(error); return false; } // Since the spec dates from 2002, we won't even bother checking the // supported version return property_reply->type != XCB_NONE && *static_cast( xcb_get_property_value(property_reply.get())) != 0; } std::optional WineXdndProxy::get_xdnd_proxy( xcb_window_t window) const noexcept { xcb_generic_error_t* error = nullptr; const xcb_get_property_cookie_t property_cookie = xcb_get_property(x11_connection.get(), false, window, xcb_xdnd_proxy_property, XCB_ATOM_WINDOW, 0, 1); const std::unique_ptr property_reply( xcb_get_property_reply(x11_connection.get(), property_cookie, &error)); if (error) { free(error); return std::nullopt; } if (property_reply->type == XCB_NONE) { return std::nullopt; } else { return *static_cast( xcb_get_property_value(property_reply.get())); } } // FIXME: See above for more context, spurious warning is generated by passing // `*last_xdnd_window` to this function #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpragmas" #pragma GCC diagnostic ignored "-Wunknown-warning-option" #pragma GCC diagnostic ignored "-Wmaybe-uninitialized" // NOLINTNEXTLINE(bugprone-easily-swappable-parameters) void WineXdndProxy::send_xdnd_message(xcb_window_t window, xcb_atom_t message_type, uint32_t data1, uint32_t data2, uint32_t data3, uint32_t data4) const noexcept { // See https://www.freedesktop.org/wiki/Specifications/XDND/#clientmessages xcb_client_message_event_t event{}; event.response_type = XCB_CLIENT_MESSAGE; event.type = message_type; // If `window` has `XdndProxy` set, then we should still mention that window // here, even though we will send the message to another window. event.window = window; event.format = 32; // THis is the source window, so the other side cna reply event.data.data32[0] = proxy_window.window; event.data.data32[1] = data1; event.data.data32[2] = data2; event.data.data32[3] = data3; event.data.data32[4] = data4; // Make sure to respect `XdndProxy` only here, as explaiend in the spec xcb_send_event(x11_connection.get(), false, get_xdnd_proxy(window).value_or(window), XCB_EVENT_MASK_NO_EVENT, reinterpret_cast(&event)); } #pragma GCC diagnostic pop void WineXdndProxy::handle_convert_selection( const xcb_selection_request_event_t& event) { xcb_change_property(x11_connection.get(), XCB_PROP_MODE_REPLACE, event.requestor, event.property, event.target, 8, dragged_files_uri_list.size(), dragged_files_uri_list.c_str()); xcb_flush(x11_connection.get()); xcb_selection_notify_event_t selection_notify_event{}; selection_notify_event.response_type = XCB_SELECTION_NOTIFY; selection_notify_event.requestor = event.requestor; selection_notify_event.selection = xcb_xdnd_selection; selection_notify_event.target = event.target; selection_notify_event.property = event.property; xcb_send_event(x11_connection.get(), false, event.requestor, XCB_NONE, reinterpret_cast(&selection_notify_event)); 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 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()); // Normalize the paths to something a bit more friendly const char* unix_path = wine_get_unix_file_name(file_name.data()); if (unix_path) { boost::system::error_code err; const fs::path cannonical_path = boost::filesystem::canonical(unix_path, err); if (err) { dragged_files.emplace_back(unix_path); } else { dragged_files.emplace_back(cannonical_path); } } } GlobalUnlock(storage.hGlobal); } break; case TYMED_FILE: { const char* unix_path = wine_get_unix_file_name(storage.lpszFileName); if (unix_path) { boost::system::error_code err; const fs::path cannonical_path = boost::filesystem::canonical(unix_path, err); if (err) { dragged_files.emplace_back(unix_path); } else { dragged_files.emplace_back(cannonical_path); } } } 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); } catch (const std::exception& error) { std::cerr << "XDND initialization failed:" << std::endl; std::cerr << error.what() << std::endl; } } #undef THROW_X11_ERROR