| 1 | // Copyright (C) 2016 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | |
| 4 | #include "qxcbdrag.h" |
| 5 | #include <xcb/xcb.h> |
| 6 | #include "qxcbconnection.h" |
| 7 | #include "qxcbclipboard.h" |
| 8 | #include "qxcbkeyboard.h" |
| 9 | #include "qxcbmime.h" |
| 10 | #include "qxcbwindow.h" |
| 11 | #include "qxcbscreen.h" |
| 12 | #include "qwindow.h" |
| 13 | #include "qxcbcursor.h" |
| 14 | #include <private/qdnd_p.h> |
| 15 | #include <qdebug.h> |
| 16 | #include <qevent.h> |
| 17 | #include <qguiapplication.h> |
| 18 | #include <qrect.h> |
| 19 | #include <qpainter.h> |
| 20 | |
| 21 | #include <qpa/qwindowsysteminterface.h> |
| 22 | |
| 23 | #include <private/qguiapplication_p.h> |
| 24 | #include <private/qshapedpixmapdndwindow_p.h> |
| 25 | #include <private/qsimpledrag_p.h> |
| 26 | #include <private/qhighdpiscaling_p.h> |
| 27 | |
| 28 | QT_BEGIN_NAMESPACE |
| 29 | |
| 30 | using namespace Qt::Literals::StringLiterals; |
| 31 | |
| 32 | const int xdnd_version = 5; |
| 33 | |
| 34 | static inline xcb_window_t xcb_window(QPlatformWindow *w) |
| 35 | { |
| 36 | return static_cast<QXcbWindow *>(w)->xcb_window(); |
| 37 | } |
| 38 | |
| 39 | static inline xcb_window_t xcb_window(QWindow *w) |
| 40 | { |
| 41 | return static_cast<QXcbWindow *>(w->handle())->xcb_window(); |
| 42 | } |
| 43 | |
| 44 | static xcb_window_t xdndProxy(QXcbConnection *c, xcb_window_t w) |
| 45 | { |
| 46 | xcb_window_t proxy = XCB_NONE; |
| 47 | |
| 48 | auto reply = Q_XCB_REPLY(xcb_get_property, c->xcb_connection(), |
| 49 | false, w, c->atom(QXcbAtom::AtomXdndProxy), XCB_ATOM_WINDOW, 0, 1); |
| 50 | |
| 51 | if (reply && reply->type == XCB_ATOM_WINDOW) |
| 52 | proxy = *((xcb_window_t *)xcb_get_property_value(R: reply.get())); |
| 53 | |
| 54 | if (proxy == XCB_NONE) |
| 55 | return proxy; |
| 56 | |
| 57 | // exists and is real? |
| 58 | reply = Q_XCB_REPLY(xcb_get_property, c->xcb_connection(), |
| 59 | false, proxy, c->atom(QXcbAtom::AtomXdndProxy), XCB_ATOM_WINDOW, 0, 1); |
| 60 | |
| 61 | if (reply && reply->type == XCB_ATOM_WINDOW) { |
| 62 | xcb_window_t p = *((xcb_window_t *)xcb_get_property_value(R: reply.get())); |
| 63 | if (proxy != p) |
| 64 | proxy = XCB_NONE; |
| 65 | } else { |
| 66 | proxy = XCB_NONE; |
| 67 | } |
| 68 | |
| 69 | return proxy; |
| 70 | } |
| 71 | |
| 72 | class QXcbDropData : public QXcbMime |
| 73 | { |
| 74 | public: |
| 75 | QXcbDropData(QXcbDrag *d); |
| 76 | ~QXcbDropData(); |
| 77 | |
| 78 | protected: |
| 79 | bool hasFormat_sys(const QString &mimeType) const override; |
| 80 | QStringList formats_sys() const override; |
| 81 | QVariant retrieveData_sys(const QString &mimeType, QMetaType type) const override; |
| 82 | |
| 83 | QVariant xdndObtainData(const QByteArray &format, QMetaType requestedType) const; |
| 84 | |
| 85 | QXcbDrag *drag; |
| 86 | }; |
| 87 | |
| 88 | |
| 89 | QXcbDrag::QXcbDrag(QXcbConnection *c) : QXcbObject(c) |
| 90 | { |
| 91 | m_dropData = new QXcbDropData(this); |
| 92 | |
| 93 | init(); |
| 94 | } |
| 95 | |
| 96 | QXcbDrag::~QXcbDrag() |
| 97 | { |
| 98 | delete m_dropData; |
| 99 | } |
| 100 | |
| 101 | void QXcbDrag::init() |
| 102 | { |
| 103 | currentWindow.clear(); |
| 104 | |
| 105 | accepted_drop_action = Qt::IgnoreAction; |
| 106 | |
| 107 | xdnd_dragsource = XCB_NONE; |
| 108 | |
| 109 | waiting_for_status = false; |
| 110 | current_target = XCB_NONE; |
| 111 | current_proxy_target = XCB_NONE; |
| 112 | |
| 113 | source_time = XCB_CURRENT_TIME; |
| 114 | target_time = XCB_CURRENT_TIME; |
| 115 | |
| 116 | QXcbCursor::queryPointer(c: connection(), virtualDesktop: ¤t_virtual_desktop, pos: nullptr); |
| 117 | drag_types.clear(); |
| 118 | |
| 119 | //current_embedding_widget = 0; |
| 120 | |
| 121 | dropped = false; |
| 122 | canceled = false; |
| 123 | |
| 124 | source_sameanswer = QRect(); |
| 125 | } |
| 126 | |
| 127 | bool QXcbDrag::eventFilter(QObject *o, QEvent *e) |
| 128 | { |
| 129 | /* We are setting a mouse grab on the QShapedPixmapWindow in order not to |
| 130 | * lose the grab when the virtual desktop changes, but |
| 131 | * QBasicDrag::eventFilter() expects the events to be coming from the |
| 132 | * window where the drag was started. */ |
| 133 | if (initiatorWindow && o == shapedPixmapWindow()) |
| 134 | o = initiatorWindow.data(); |
| 135 | return QBasicDrag::eventFilter(o, e); |
| 136 | } |
| 137 | |
| 138 | void QXcbDrag::startDrag() |
| 139 | { |
| 140 | init(); |
| 141 | |
| 142 | qCDebug(lcQpaXDnd) << "starting drag where source:" << connection()->qtSelectionOwner(); |
| 143 | xcb_set_selection_owner(c: xcb_connection(), owner: connection()->qtSelectionOwner(), |
| 144 | selection: atom(atom: QXcbAtom::AtomXdndSelection), time: connection()->time()); |
| 145 | |
| 146 | QStringList fmts = QXcbMime::formatsHelper(data: drag()->mimeData()); |
| 147 | for (int i = 0; i < fmts.size(); ++i) { |
| 148 | QList<xcb_atom_t> atoms = QXcbMime::mimeAtomsForFormat(connection: connection(), format: fmts.at(i)); |
| 149 | for (int j = 0; j < atoms.size(); ++j) { |
| 150 | if (!drag_types.contains(t: atoms.at(i: j))) |
| 151 | drag_types.append(t: atoms.at(i: j)); |
| 152 | } |
| 153 | } |
| 154 | |
| 155 | if (drag_types.size() > 3) |
| 156 | xcb_change_property(c: xcb_connection(), mode: XCB_PROP_MODE_REPLACE, window: connection()->qtSelectionOwner(), |
| 157 | property: atom(atom: QXcbAtom::AtomXdndTypelist), |
| 158 | type: XCB_ATOM_ATOM, format: 32, data_len: drag_types.size(), data: (const void *)drag_types.constData()); |
| 159 | |
| 160 | setUseCompositing(current_virtual_desktop->compositingActive()); |
| 161 | setScreen(current_virtual_desktop->screens().constFirst()->screen()); |
| 162 | initiatorWindow = QGuiApplicationPrivate::currentMouseWindow; |
| 163 | QBasicDrag::startDrag(); |
| 164 | if (connection()->mouseGrabber() == nullptr) |
| 165 | shapedPixmapWindow()->setMouseGrabEnabled(true); |
| 166 | |
| 167 | auto nativePixelPos = QHighDpi::toNativePixels(value: QCursor::pos(), context: initiatorWindow.data()); |
| 168 | move(globalPos: nativePixelPos, b: QGuiApplication::mouseButtons(), mods: QGuiApplication::keyboardModifiers()); |
| 169 | } |
| 170 | |
| 171 | void QXcbDrag::endDrag() |
| 172 | { |
| 173 | QBasicDrag::endDrag(); |
| 174 | if (!dropped && !canceled && canDrop()) { |
| 175 | // Set executed drop action when dropping outside application. |
| 176 | setExecutedDropAction(accepted_drop_action); |
| 177 | } |
| 178 | initiatorWindow.clear(); |
| 179 | } |
| 180 | |
| 181 | Qt::DropAction QXcbDrag::defaultAction(Qt::DropActions possibleActions, Qt::KeyboardModifiers modifiers) const |
| 182 | { |
| 183 | if (currentDrag() || drop_actions.isEmpty()) |
| 184 | return QBasicDrag::defaultAction(possibleActions, modifiers); |
| 185 | |
| 186 | return toDropAction(atom: drop_actions.first()); |
| 187 | } |
| 188 | |
| 189 | void QXcbDrag::handlePropertyNotifyEvent(const xcb_property_notify_event_t *event) |
| 190 | { |
| 191 | if (event->window != xdnd_dragsource || event->atom != atom(atom: QXcbAtom::AtomXdndActionList)) |
| 192 | return; |
| 193 | |
| 194 | readActionList(); |
| 195 | } |
| 196 | |
| 197 | static |
| 198 | bool windowInteractsWithPosition(xcb_connection_t *connection, const QPoint & pos, xcb_window_t w, xcb_shape_sk_t shapeType) |
| 199 | { |
| 200 | bool interacts = false; |
| 201 | auto reply = Q_XCB_REPLY(xcb_shape_get_rectangles, connection, w, shapeType); |
| 202 | if (reply) { |
| 203 | xcb_rectangle_t *rectangles = xcb_shape_get_rectangles_rectangles(R: reply.get()); |
| 204 | if (rectangles) { |
| 205 | const int nRectangles = xcb_shape_get_rectangles_rectangles_length(R: reply.get()); |
| 206 | for (int i = 0; !interacts && i < nRectangles; ++i) { |
| 207 | interacts = QRect(rectangles[i].x, rectangles[i].y, rectangles[i].width, rectangles[i].height).contains(p: pos); |
| 208 | } |
| 209 | } |
| 210 | } |
| 211 | |
| 212 | return interacts; |
| 213 | } |
| 214 | |
| 215 | xcb_window_t QXcbDrag::findRealWindow(const QPoint & pos, xcb_window_t w, int md, bool ignoreNonXdndAwareWindows) |
| 216 | { |
| 217 | if (w == shapedPixmapWindow()->handle()->winId()) |
| 218 | return 0; |
| 219 | |
| 220 | if (md) { |
| 221 | auto reply = Q_XCB_REPLY(xcb_get_window_attributes, xcb_connection(), w); |
| 222 | if (!reply) |
| 223 | return 0; |
| 224 | |
| 225 | if (reply->map_state != XCB_MAP_STATE_VIEWABLE) |
| 226 | return 0; |
| 227 | |
| 228 | auto greply = Q_XCB_REPLY(xcb_get_geometry, xcb_connection(), w); |
| 229 | if (!greply) |
| 230 | return 0; |
| 231 | |
| 232 | QRect windowRect(greply->x, greply->y, greply->width, greply->height); |
| 233 | if (windowRect.contains(p: pos)) { |
| 234 | bool windowContainsMouse = !ignoreNonXdndAwareWindows; |
| 235 | { |
| 236 | auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), |
| 237 | false, w, connection()->atom(QXcbAtom::AtomXdndAware), |
| 238 | XCB_GET_PROPERTY_TYPE_ANY, 0, 0); |
| 239 | bool isAware = reply && reply->type != XCB_NONE; |
| 240 | if (isAware) { |
| 241 | const QPoint relPos = pos - windowRect.topLeft(); |
| 242 | // When ShapeInput and ShapeBounding are not set they return a single rectangle with the geometry of the window, this is why we |
| 243 | // need to check both here so that in the case one is set and the other is not we still get the correct result. |
| 244 | if (connection()->hasInputShape()) |
| 245 | windowContainsMouse = windowInteractsWithPosition(connection: xcb_connection(), pos: relPos, w, shapeType: XCB_SHAPE_SK_INPUT); |
| 246 | if (windowContainsMouse && connection()->hasXShape()) |
| 247 | windowContainsMouse = windowInteractsWithPosition(connection: xcb_connection(), pos: relPos, w, shapeType: XCB_SHAPE_SK_BOUNDING); |
| 248 | if (!connection()->hasInputShape() && !connection()->hasXShape()) |
| 249 | windowContainsMouse = true; |
| 250 | if (windowContainsMouse) |
| 251 | return w; |
| 252 | } |
| 253 | } |
| 254 | |
| 255 | auto reply = Q_XCB_REPLY(xcb_query_tree, xcb_connection(), w); |
| 256 | if (!reply) |
| 257 | return 0; |
| 258 | int nc = xcb_query_tree_children_length(R: reply.get()); |
| 259 | xcb_window_t *c = xcb_query_tree_children(R: reply.get()); |
| 260 | |
| 261 | xcb_window_t r = 0; |
| 262 | for (uint i = nc; !r && i--;) |
| 263 | r = findRealWindow(pos: pos - windowRect.topLeft(), w: c[i], md: md-1, ignoreNonXdndAwareWindows); |
| 264 | |
| 265 | if (r) |
| 266 | return r; |
| 267 | |
| 268 | // We didn't find a client window! Just use the |
| 269 | // innermost window. |
| 270 | |
| 271 | // No children! |
| 272 | if (!windowContainsMouse) |
| 273 | return 0; |
| 274 | else |
| 275 | return w; |
| 276 | } |
| 277 | } |
| 278 | return 0; |
| 279 | } |
| 280 | |
| 281 | bool QXcbDrag::findXdndAwareTarget(const QPoint &globalPos, xcb_window_t *target_out) |
| 282 | { |
| 283 | xcb_window_t rootwin = current_virtual_desktop->root(); |
| 284 | auto translate = Q_XCB_REPLY(xcb_translate_coordinates, xcb_connection(), |
| 285 | rootwin, rootwin, globalPos.x(), globalPos.y()); |
| 286 | if (!translate) |
| 287 | return false; |
| 288 | |
| 289 | xcb_window_t target = translate->child; |
| 290 | int lx = translate->dst_x; |
| 291 | int ly = translate->dst_y; |
| 292 | |
| 293 | if (target && target != rootwin) { |
| 294 | xcb_window_t src = rootwin; |
| 295 | while (target != 0) { |
| 296 | qCDebug(lcQpaXDnd) << "checking target for XdndAware" << target; |
| 297 | |
| 298 | auto translate = Q_XCB_REPLY(xcb_translate_coordinates, xcb_connection(), |
| 299 | src, target, lx, ly); |
| 300 | if (!translate) { |
| 301 | target = 0; |
| 302 | break; |
| 303 | } |
| 304 | lx = translate->dst_x; |
| 305 | ly = translate->dst_y; |
| 306 | src = target; |
| 307 | xcb_window_t child = translate->child; |
| 308 | |
| 309 | auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, target, |
| 310 | atom(QXcbAtom::AtomXdndAware), XCB_GET_PROPERTY_TYPE_ANY, 0, 0); |
| 311 | bool aware = reply && reply->type != XCB_NONE; |
| 312 | if (aware) { |
| 313 | qCDebug(lcQpaXDnd) << "found XdndAware on" << target; |
| 314 | break; |
| 315 | } |
| 316 | |
| 317 | target = child; |
| 318 | } |
| 319 | |
| 320 | if (!target || target == shapedPixmapWindow()->handle()->winId()) { |
| 321 | qCDebug(lcQpaXDnd) << "need to find real window" ; |
| 322 | target = findRealWindow(pos: globalPos, w: rootwin, md: 6, ignoreNonXdndAwareWindows: true); |
| 323 | if (target == 0) |
| 324 | target = findRealWindow(pos: globalPos, w: rootwin, md: 6, ignoreNonXdndAwareWindows: false); |
| 325 | qCDebug(lcQpaXDnd) << "real window found" << target; |
| 326 | } |
| 327 | } |
| 328 | |
| 329 | *target_out = target; |
| 330 | return true; |
| 331 | } |
| 332 | |
| 333 | void QXcbDrag::move(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardModifiers mods) |
| 334 | { |
| 335 | // currentDrag() might be deleted while 'drag' is progressing |
| 336 | if (!currentDrag()) { |
| 337 | cancel(); |
| 338 | return; |
| 339 | } |
| 340 | // The source sends XdndEnter and XdndPosition to the target. |
| 341 | if (source_sameanswer.contains(p: globalPos) && source_sameanswer.isValid()) |
| 342 | return; |
| 343 | |
| 344 | QXcbVirtualDesktop *virtualDesktop = nullptr; |
| 345 | QPoint cursorPos; |
| 346 | QXcbCursor::queryPointer(c: connection(), virtualDesktop: &virtualDesktop, pos: &cursorPos); |
| 347 | QXcbScreen *screen = virtualDesktop->screenAt(pos: cursorPos); |
| 348 | QPoint deviceIndependentPos = QHighDpiScaling::mapPositionFromNative(pos: globalPos, platformScreen: screen); |
| 349 | |
| 350 | if (virtualDesktop != current_virtual_desktop) { |
| 351 | setUseCompositing(virtualDesktop->compositingActive()); |
| 352 | recreateShapedPixmapWindow(screen: static_cast<QPlatformScreen*>(screen)->screen(), pos: deviceIndependentPos); |
| 353 | if (connection()->mouseGrabber() == nullptr) |
| 354 | shapedPixmapWindow()->setMouseGrabEnabled(true); |
| 355 | |
| 356 | current_virtual_desktop = virtualDesktop; |
| 357 | } else { |
| 358 | QBasicDrag::moveShapedPixmapWindow(deviceIndependentPosition: deviceIndependentPos); |
| 359 | } |
| 360 | |
| 361 | xcb_window_t target; |
| 362 | if (!findXdndAwareTarget(globalPos, target_out: &target)) |
| 363 | return; |
| 364 | |
| 365 | QXcbWindow *w = nullptr; |
| 366 | if (target) { |
| 367 | w = connection()->platformWindowFromId(id: target); |
| 368 | if (w && (w->window()->type() == Qt::Desktop) /*&& !w->acceptDrops()*/) |
| 369 | w = nullptr; |
| 370 | } else { |
| 371 | w = nullptr; |
| 372 | target = current_virtual_desktop->root(); |
| 373 | } |
| 374 | |
| 375 | xcb_window_t proxy_target = xdndProxy(c: connection(), w: target); |
| 376 | if (!proxy_target) |
| 377 | proxy_target = target; |
| 378 | int target_version = 1; |
| 379 | |
| 380 | if (proxy_target) { |
| 381 | auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), |
| 382 | false, proxy_target, |
| 383 | atom(QXcbAtom::AtomXdndAware), XCB_GET_PROPERTY_TYPE_ANY, 0, 1); |
| 384 | if (!reply || reply->type == XCB_NONE) { |
| 385 | target = 0; |
| 386 | } else { |
| 387 | target_version = *(uint32_t *)xcb_get_property_value(R: reply.get()); |
| 388 | target_version = qMin(a: xdnd_version, b: target_version ? target_version : 1); |
| 389 | } |
| 390 | } |
| 391 | |
| 392 | if (target != current_target) { |
| 393 | if (current_target) |
| 394 | send_leave(); |
| 395 | |
| 396 | current_target = target; |
| 397 | current_proxy_target = proxy_target; |
| 398 | if (target) { |
| 399 | int flags = target_version << 24; |
| 400 | if (drag_types.size() > 3) |
| 401 | flags |= 0x0001; |
| 402 | |
| 403 | xcb_client_message_event_t enter; |
| 404 | enter.response_type = XCB_CLIENT_MESSAGE; |
| 405 | enter.sequence = 0; |
| 406 | enter.window = target; |
| 407 | enter.format = 32; |
| 408 | enter.type = atom(atom: QXcbAtom::AtomXdndEnter); |
| 409 | enter.data.data32[0] = connection()->qtSelectionOwner(); |
| 410 | enter.data.data32[1] = flags; |
| 411 | enter.data.data32[2] = drag_types.size() > 0 ? drag_types.at(i: 0) : 0; |
| 412 | enter.data.data32[3] = drag_types.size() > 1 ? drag_types.at(i: 1) : 0; |
| 413 | enter.data.data32[4] = drag_types.size() > 2 ? drag_types.at(i: 2) : 0; |
| 414 | // provisionally set the rectangle to 5x5 pixels... |
| 415 | source_sameanswer = QRect(globalPos.x() - 2, globalPos.y() - 2 , 5, 5); |
| 416 | |
| 417 | qCDebug(lcQpaXDnd) << "sending XdndEnter to target:" << target; |
| 418 | |
| 419 | if (w) |
| 420 | handleEnter(window: w, event: &enter, proxy: current_proxy_target); |
| 421 | else if (target) |
| 422 | xcb_send_event(c: xcb_connection(), propagate: false, destination: proxy_target, event_mask: XCB_EVENT_MASK_NO_EVENT, event: (const char *)&enter); |
| 423 | waiting_for_status = false; |
| 424 | } |
| 425 | } |
| 426 | |
| 427 | if (waiting_for_status) |
| 428 | return; |
| 429 | |
| 430 | if (target) { |
| 431 | waiting_for_status = true; |
| 432 | // The source sends a ClientMessage of type XdndPosition. This tells the target the |
| 433 | // position of the mouse and the action that the user requested. |
| 434 | xcb_client_message_event_t move; |
| 435 | move.response_type = XCB_CLIENT_MESSAGE; |
| 436 | move.sequence = 0; |
| 437 | move.window = target; |
| 438 | move.format = 32; |
| 439 | move.type = atom(atom: QXcbAtom::AtomXdndPosition); |
| 440 | move.data.data32[0] = connection()->qtSelectionOwner(); |
| 441 | move.data.data32[1] = 0; // flags |
| 442 | move.data.data32[2] = (globalPos.x() << 16) + globalPos.y(); |
| 443 | move.data.data32[3] = connection()->time(); |
| 444 | const auto supportedActions = currentDrag()->supportedActions(); |
| 445 | const auto requestedAction = defaultAction(possibleActions: supportedActions, modifiers: mods); |
| 446 | move.data.data32[4] = toXdndAction(a: requestedAction); |
| 447 | |
| 448 | qCDebug(lcQpaXDnd) << "sending XdndPosition to target:" << target; |
| 449 | |
| 450 | source_time = connection()->time(); |
| 451 | |
| 452 | if (w) { |
| 453 | handle_xdnd_position(w, event: &move, b, mods); |
| 454 | } else { |
| 455 | setActionList(requestedAction, supportedActions); |
| 456 | xcb_send_event(c: xcb_connection(), propagate: false, destination: proxy_target, event_mask: XCB_EVENT_MASK_NO_EVENT, event: (const char *)&move); |
| 457 | } |
| 458 | } |
| 459 | |
| 460 | static const bool isUnity = qgetenv(varName: "XDG_CURRENT_DESKTOP" ).toLower() == "unity" ; |
| 461 | if (isUnity && xdndCollectionWindow == XCB_NONE) { |
| 462 | QString name = QXcbWindow::windowTitle(conn: connection(), window: target); |
| 463 | if (name == "XdndCollectionWindowImp"_L1 ) |
| 464 | xdndCollectionWindow = target; |
| 465 | } |
| 466 | if (target == xdndCollectionWindow) { |
| 467 | setCanDrop(false); |
| 468 | updateCursor(action: Qt::IgnoreAction); |
| 469 | } |
| 470 | } |
| 471 | |
| 472 | void QXcbDrag::drop(const QPoint &globalPos, Qt::MouseButtons b, Qt::KeyboardModifiers mods) |
| 473 | { |
| 474 | // XdndDrop is sent from source to target to complete the drop. |
| 475 | QBasicDrag::drop(globalPos, b, mods); |
| 476 | |
| 477 | if (!current_target) |
| 478 | return; |
| 479 | |
| 480 | xcb_client_message_event_t drop; |
| 481 | drop.response_type = XCB_CLIENT_MESSAGE; |
| 482 | drop.sequence = 0; |
| 483 | drop.window = current_target; |
| 484 | drop.format = 32; |
| 485 | drop.type = atom(atom: QXcbAtom::AtomXdndDrop); |
| 486 | drop.data.data32[0] = connection()->qtSelectionOwner(); |
| 487 | drop.data.data32[1] = 0; // flags |
| 488 | drop.data.data32[2] = connection()->time(); |
| 489 | |
| 490 | drop.data.data32[3] = 0; |
| 491 | drop.data.data32[4] = currentDrag()->supportedActions(); |
| 492 | |
| 493 | QXcbWindow *w = connection()->platformWindowFromId(id: current_proxy_target); |
| 494 | |
| 495 | if (w && w->window()->type() == Qt::Desktop) // && !w->acceptDrops() |
| 496 | w = nullptr; |
| 497 | |
| 498 | Transaction t = { |
| 499 | .timestamp: connection()->time(), |
| 500 | .target: current_target, |
| 501 | .proxy_target: current_proxy_target, |
| 502 | .targetWindow: w, |
| 503 | // current_embeddig_widget, |
| 504 | .drag: currentDrag(), |
| 505 | .time: QTime::currentTime() |
| 506 | }; |
| 507 | transactions.append(t); |
| 508 | |
| 509 | // timer is needed only for drops that came from other processes. |
| 510 | if (!t.targetWindow && !cleanup_timer.isActive()) |
| 511 | cleanup_timer.start(duration: XdndDropTransactionTimeout, obj: this); |
| 512 | |
| 513 | qCDebug(lcQpaXDnd) << "sending drop to target:" << current_target; |
| 514 | |
| 515 | if (w) { |
| 516 | handleDrop(w, event: &drop, b, mods); |
| 517 | } else { |
| 518 | xcb_send_event(c: xcb_connection(), propagate: false, destination: current_proxy_target, event_mask: XCB_EVENT_MASK_NO_EVENT, event: (const char *)&drop); |
| 519 | } |
| 520 | } |
| 521 | |
| 522 | Qt::DropAction QXcbDrag::toDropAction(xcb_atom_t a) const |
| 523 | { |
| 524 | if (a == atom(atom: QXcbAtom::AtomXdndActionCopy) || a == 0) |
| 525 | return Qt::CopyAction; |
| 526 | if (a == atom(atom: QXcbAtom::AtomXdndActionLink)) |
| 527 | return Qt::LinkAction; |
| 528 | if (a == atom(atom: QXcbAtom::AtomXdndActionMove)) |
| 529 | return Qt::MoveAction; |
| 530 | return Qt::CopyAction; |
| 531 | } |
| 532 | |
| 533 | Qt::DropActions QXcbDrag::toDropActions(const QList<xcb_atom_t> &atoms) const |
| 534 | { |
| 535 | Qt::DropActions actions; |
| 536 | for (const auto actionAtom : atoms) { |
| 537 | if (actionAtom != atom(atom: QXcbAtom::AtomXdndActionAsk)) |
| 538 | actions |= toDropAction(a: actionAtom); |
| 539 | } |
| 540 | return actions; |
| 541 | } |
| 542 | |
| 543 | xcb_atom_t QXcbDrag::toXdndAction(Qt::DropAction a) const |
| 544 | { |
| 545 | switch (a) { |
| 546 | case Qt::CopyAction: |
| 547 | return atom(atom: QXcbAtom::AtomXdndActionCopy); |
| 548 | case Qt::LinkAction: |
| 549 | return atom(atom: QXcbAtom::AtomXdndActionLink); |
| 550 | case Qt::MoveAction: |
| 551 | case Qt::TargetMoveAction: |
| 552 | return atom(atom: QXcbAtom::AtomXdndActionMove); |
| 553 | case Qt::IgnoreAction: |
| 554 | return XCB_NONE; |
| 555 | default: |
| 556 | return atom(atom: QXcbAtom::AtomXdndActionCopy); |
| 557 | } |
| 558 | } |
| 559 | |
| 560 | void QXcbDrag::readActionList() |
| 561 | { |
| 562 | drop_actions.clear(); |
| 563 | auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, xdnd_dragsource, |
| 564 | atom(QXcbAtom::AtomXdndActionList), XCB_ATOM_ATOM, |
| 565 | 0, 1024); |
| 566 | if (reply && reply->type != XCB_NONE && reply->format == 32) { |
| 567 | int length = xcb_get_property_value_length(R: reply.get()) / 4; |
| 568 | |
| 569 | xcb_atom_t *atoms = (xcb_atom_t *)xcb_get_property_value(R: reply.get()); |
| 570 | for (int i = 0; i < length; ++i) |
| 571 | drop_actions.append(t: atoms[i]); |
| 572 | } |
| 573 | } |
| 574 | |
| 575 | void QXcbDrag::setActionList(Qt::DropAction requestedAction, Qt::DropActions supportedActions) |
| 576 | { |
| 577 | #ifndef QT_NO_CLIPBOARD |
| 578 | QList<xcb_atom_t> actions; |
| 579 | if (requestedAction != Qt::IgnoreAction) |
| 580 | actions.append(t: toXdndAction(a: requestedAction)); |
| 581 | |
| 582 | auto checkAppend = [this, requestedAction, supportedActions, &actions](Qt::DropAction action) { |
| 583 | if (requestedAction != action && supportedActions & action) |
| 584 | actions.append(t: toXdndAction(a: action)); |
| 585 | }; |
| 586 | |
| 587 | checkAppend(Qt::CopyAction); |
| 588 | checkAppend(Qt::MoveAction); |
| 589 | checkAppend(Qt::LinkAction); |
| 590 | |
| 591 | if (current_actions != actions) { |
| 592 | xcb_change_property(c: xcb_connection(), mode: XCB_PROP_MODE_REPLACE, window: connection()->qtSelectionOwner(), |
| 593 | property: atom(atom: QXcbAtom::AtomXdndActionList), |
| 594 | type: XCB_ATOM_ATOM, format: 32, data_len: actions.size(), data: actions.constData()); |
| 595 | current_actions = actions; |
| 596 | } |
| 597 | #else |
| 598 | Q_UNUSED(requestedAction) |
| 599 | Q_UNUSED(supportedActions) |
| 600 | #endif |
| 601 | } |
| 602 | |
| 603 | void QXcbDrag::startListeningForActionListChanges() |
| 604 | { |
| 605 | connection()->addWindowEventListener(id: xdnd_dragsource, eventListener: this); |
| 606 | const uint32_t event_mask[] = { XCB_EVENT_MASK_PROPERTY_CHANGE }; |
| 607 | xcb_change_window_attributes(c: xcb_connection(), window: xdnd_dragsource, value_mask: XCB_CW_EVENT_MASK, value_list: event_mask); |
| 608 | } |
| 609 | |
| 610 | void QXcbDrag::stopListeningForActionListChanges() |
| 611 | { |
| 612 | const uint32_t event_mask[] = { XCB_EVENT_MASK_NO_EVENT }; |
| 613 | xcb_change_window_attributes(c: xcb_connection(), window: xdnd_dragsource, value_mask: XCB_CW_EVENT_MASK, value_list: event_mask); |
| 614 | connection()->removeWindowEventListener(id: xdnd_dragsource); |
| 615 | } |
| 616 | |
| 617 | int QXcbDrag::findTransactionByWindow(xcb_window_t window) |
| 618 | { |
| 619 | int at = -1; |
| 620 | for (int i = 0; i < transactions.size(); ++i) { |
| 621 | const Transaction &t = transactions.at(i); |
| 622 | if (t.target == window || t.proxy_target == window) { |
| 623 | at = i; |
| 624 | break; |
| 625 | } |
| 626 | } |
| 627 | return at; |
| 628 | } |
| 629 | |
| 630 | int QXcbDrag::findTransactionByTime(xcb_timestamp_t timestamp) |
| 631 | { |
| 632 | int at = -1; |
| 633 | for (int i = 0; i < transactions.size(); ++i) { |
| 634 | const Transaction &t = transactions.at(i); |
| 635 | if (t.timestamp == timestamp) { |
| 636 | at = i; |
| 637 | break; |
| 638 | } |
| 639 | } |
| 640 | return at; |
| 641 | } |
| 642 | |
| 643 | #if 0 |
| 644 | // for embedding only |
| 645 | static QWidget* current_embedding_widget = nullptr; |
| 646 | static xcb_client_message_event_t last_enter_event; |
| 647 | |
| 648 | |
| 649 | static bool checkEmbedded(QWidget* w, const XEvent* xe) |
| 650 | { |
| 651 | if (!w) |
| 652 | return false; |
| 653 | |
| 654 | if (current_embedding_widget != 0 && current_embedding_widget != w) { |
| 655 | current_target = ((QExtraWidget*)current_embedding_widget)->extraData()->xDndProxy; |
| 656 | current_proxy_target = current_target; |
| 657 | qt_xdnd_send_leave(); |
| 658 | current_target = 0; |
| 659 | current_proxy_target = 0; |
| 660 | current_embedding_widget = 0; |
| 661 | } |
| 662 | |
| 663 | QWExtra* extra = ((QExtraWidget*)w)->extraData(); |
| 664 | if (extra && extra->xDndProxy != 0) { |
| 665 | |
| 666 | if (current_embedding_widget != w) { |
| 667 | |
| 668 | last_enter_event.xany.window = extra->xDndProxy; |
| 669 | XSendEvent(X11->display, extra->xDndProxy, False, NoEventMask, &last_enter_event); |
| 670 | current_embedding_widget = w; |
| 671 | } |
| 672 | |
| 673 | ((XEvent*)xe)->xany.window = extra->xDndProxy; |
| 674 | XSendEvent(X11->display, extra->xDndProxy, False, NoEventMask, (XEvent*)xe); |
| 675 | if (currentWindow != w) { |
| 676 | currentWindow = w; |
| 677 | } |
| 678 | return true; |
| 679 | } |
| 680 | current_embedding_widget = 0; |
| 681 | return false; |
| 682 | } |
| 683 | #endif |
| 684 | |
| 685 | void QXcbDrag::handleEnter(QPlatformWindow *, const xcb_client_message_event_t *event, xcb_window_t proxy) |
| 686 | { |
| 687 | // The target receives XdndEnter. |
| 688 | qCDebug(lcQpaXDnd) << "target:" << event->window << "received XdndEnter" ; |
| 689 | |
| 690 | xdnd_types.clear(); |
| 691 | |
| 692 | int version = (int)(event->data.data32[1] >> 24); |
| 693 | if (version > xdnd_version) |
| 694 | return; |
| 695 | |
| 696 | xdnd_dragsource = event->data.data32[0]; |
| 697 | startListeningForActionListChanges(); |
| 698 | readActionList(); |
| 699 | |
| 700 | if (!proxy) |
| 701 | proxy = xdndProxy(c: connection(), w: xdnd_dragsource); |
| 702 | current_proxy_target = proxy ? proxy : xdnd_dragsource; |
| 703 | |
| 704 | if (event->data.data32[1] & 1) { |
| 705 | // get the types from XdndTypeList |
| 706 | auto reply = Q_XCB_REPLY(xcb_get_property, xcb_connection(), false, xdnd_dragsource, |
| 707 | atom(QXcbAtom::AtomXdndTypelist), XCB_ATOM_ATOM, |
| 708 | 0, xdnd_max_type); |
| 709 | if (reply && reply->type != XCB_NONE && reply->format == 32) { |
| 710 | int length = xcb_get_property_value_length(R: reply.get()) / 4; |
| 711 | if (length > xdnd_max_type) |
| 712 | length = xdnd_max_type; |
| 713 | |
| 714 | xcb_atom_t *atoms = (xcb_atom_t *)xcb_get_property_value(R: reply.get()); |
| 715 | xdnd_types.reserve(asize: length); |
| 716 | for (int i = 0; i < length; ++i) |
| 717 | xdnd_types.append(t: atoms[i]); |
| 718 | } |
| 719 | } else { |
| 720 | // get the types from the message |
| 721 | for(int i = 2; i < 5; i++) { |
| 722 | if (event->data.data32[i]) |
| 723 | xdnd_types.append(t: event->data.data32[i]); |
| 724 | } |
| 725 | } |
| 726 | for(int i = 0; i < xdnd_types.size(); ++i) |
| 727 | qCDebug(lcQpaXDnd) << " " << connection()->atomName(atom: xdnd_types.at(i)); |
| 728 | } |
| 729 | |
| 730 | void QXcbDrag::handle_xdnd_position(QPlatformWindow *w, const xcb_client_message_event_t *e, |
| 731 | Qt::MouseButtons b, Qt::KeyboardModifiers mods) |
| 732 | { |
| 733 | // The target receives XdndPosition. The target window must determine which widget the mouse |
| 734 | // is in and ask it whether or not it will accept the drop. |
| 735 | qCDebug(lcQpaXDnd) << "target:" << e->window << "received XdndPosition" ; |
| 736 | |
| 737 | QPoint p((e->data.data32[2] & 0xffff0000) >> 16, e->data.data32[2] & 0x0000ffff); |
| 738 | Q_ASSERT(w); |
| 739 | QRect geometry = w->geometry(); |
| 740 | p -= w->isEmbedded() ? w->mapToGlobal(pos: geometry.topLeft()) : geometry.topLeft(); |
| 741 | |
| 742 | if (!w || !w->window() || (w->window()->type() == Qt::Desktop)) |
| 743 | return; |
| 744 | |
| 745 | if (Q_UNLIKELY(e->data.data32[0] != xdnd_dragsource)) { |
| 746 | qCDebug(lcQpaXDnd, "xdnd drag position from unexpected source (%x not %x)" , |
| 747 | e->data.data32[0], xdnd_dragsource); |
| 748 | return; |
| 749 | } |
| 750 | |
| 751 | currentPosition = p; |
| 752 | currentWindow = w->window(); |
| 753 | |
| 754 | // timestamp from the source |
| 755 | if (e->data.data32[3] != XCB_NONE) { |
| 756 | target_time = e->data.data32[3]; |
| 757 | } |
| 758 | |
| 759 | QMimeData *dropData = nullptr; |
| 760 | Qt::DropActions supported_actions = Qt::IgnoreAction; |
| 761 | if (currentDrag()) { |
| 762 | dropData = currentDrag()->mimeData(); |
| 763 | supported_actions = currentDrag()->supportedActions(); |
| 764 | } else { |
| 765 | dropData = m_dropData; |
| 766 | supported_actions = toDropActions(atoms: drop_actions); |
| 767 | if (e->data.data32[4] != atom(atom: QXcbAtom::AtomXdndActionAsk)) |
| 768 | supported_actions |= Qt::DropActions(toDropAction(a: e->data.data32[4])); |
| 769 | } |
| 770 | |
| 771 | auto buttons = currentDrag() ? b : connection()->queryMouseButtons(); |
| 772 | auto modifiers = currentDrag() ? mods : connection()->keyboard()->queryKeyboardModifiers(); |
| 773 | |
| 774 | QPlatformDragQtResponse qt_response = QWindowSystemInterface::handleDrag( |
| 775 | window: w->window(), dropData, p, supportedActions: supported_actions, buttons, modifiers); |
| 776 | |
| 777 | // ### FIXME ? - answerRect appears to be unused. |
| 778 | QRect answerRect(p + geometry.topLeft(), QSize(1,1)); |
| 779 | answerRect = qt_response.answerRect().translated(p: geometry.topLeft()).intersected(other: geometry); |
| 780 | |
| 781 | // The target sends a ClientMessage of type XdndStatus. This tells the source whether or not |
| 782 | // it will accept the drop, and, if so, what action will be taken. It also includes a rectangle |
| 783 | // that means "don't send another XdndPosition message until the mouse moves out of here". |
| 784 | xcb_client_message_event_t response; |
| 785 | response.response_type = XCB_CLIENT_MESSAGE; |
| 786 | response.sequence = 0; |
| 787 | response.window = xdnd_dragsource; |
| 788 | response.format = 32; |
| 789 | response.type = atom(atom: QXcbAtom::AtomXdndStatus); |
| 790 | response.data.data32[0] = xcb_window(w); |
| 791 | response.data.data32[1] = qt_response.isAccepted(); // flags |
| 792 | response.data.data32[2] = 0; // x, y |
| 793 | response.data.data32[3] = 0; // w, h |
| 794 | response.data.data32[4] = toXdndAction(a: qt_response.acceptedAction()); // action |
| 795 | |
| 796 | accepted_drop_action = qt_response.acceptedAction(); |
| 797 | |
| 798 | if (answerRect.left() < 0) |
| 799 | answerRect.setLeft(0); |
| 800 | if (answerRect.right() > 4096) |
| 801 | answerRect.setRight(4096); |
| 802 | if (answerRect.top() < 0) |
| 803 | answerRect.setTop(0); |
| 804 | if (answerRect.bottom() > 4096) |
| 805 | answerRect.setBottom(4096); |
| 806 | if (answerRect.width() < 0) |
| 807 | answerRect.setWidth(0); |
| 808 | if (answerRect.height() < 0) |
| 809 | answerRect.setHeight(0); |
| 810 | |
| 811 | // reset |
| 812 | target_time = XCB_CURRENT_TIME; |
| 813 | |
| 814 | qCDebug(lcQpaXDnd) << "sending XdndStatus to source:" << xdnd_dragsource; |
| 815 | |
| 816 | if (xdnd_dragsource == connection()->qtSelectionOwner()) |
| 817 | handle_xdnd_status(event: &response); |
| 818 | else |
| 819 | xcb_send_event(c: xcb_connection(), propagate: false, destination: current_proxy_target, |
| 820 | event_mask: XCB_EVENT_MASK_NO_EVENT, event: (const char *)&response); |
| 821 | } |
| 822 | |
| 823 | namespace |
| 824 | { |
| 825 | class ClientMessageScanner { |
| 826 | public: |
| 827 | ClientMessageScanner(xcb_atom_t a) : atom(a) {} |
| 828 | xcb_atom_t atom; |
| 829 | bool operator() (xcb_generic_event_t *event, int type) const { |
| 830 | if (type != XCB_CLIENT_MESSAGE) |
| 831 | return false; |
| 832 | auto clientMessage = reinterpret_cast<xcb_client_message_event_t *>(event); |
| 833 | return clientMessage->type == atom; |
| 834 | } |
| 835 | }; |
| 836 | } |
| 837 | |
| 838 | void QXcbDrag::handlePosition(QPlatformWindow * w, const xcb_client_message_event_t *event) |
| 839 | { |
| 840 | xcb_client_message_event_t *lastEvent = const_cast<xcb_client_message_event_t *>(event); |
| 841 | ClientMessageScanner scanner(atom(atom: QXcbAtom::AtomXdndPosition)); |
| 842 | while (auto nextEvent = connection()->eventQueue()->peek(peeker&: scanner)) { |
| 843 | if (lastEvent != event) |
| 844 | free(ptr: lastEvent); |
| 845 | lastEvent = reinterpret_cast<xcb_client_message_event_t *>(nextEvent); |
| 846 | } |
| 847 | |
| 848 | handle_xdnd_position(w, e: lastEvent); |
| 849 | if (lastEvent != event) |
| 850 | free(ptr: lastEvent); |
| 851 | } |
| 852 | |
| 853 | void QXcbDrag::handle_xdnd_status(const xcb_client_message_event_t *event) |
| 854 | { |
| 855 | // The source receives XdndStatus. It can use the action to change the cursor to indicate |
| 856 | // whether or not the user's requested action will be performed. |
| 857 | qCDebug(lcQpaXDnd) << "source:" << event->window << "received XdndStatus" ; |
| 858 | waiting_for_status = false; |
| 859 | // ignore late status messages |
| 860 | if (event->data.data32[0] && event->data.data32[0] != current_target) |
| 861 | return; |
| 862 | |
| 863 | const bool dropPossible = event->data.data32[1]; |
| 864 | setCanDrop(dropPossible); |
| 865 | |
| 866 | if (dropPossible) { |
| 867 | accepted_drop_action = toDropAction(a: event->data.data32[4]); |
| 868 | updateCursor(action: accepted_drop_action); |
| 869 | } else { |
| 870 | updateCursor(action: Qt::IgnoreAction); |
| 871 | } |
| 872 | |
| 873 | if ((event->data.data32[1] & 2) == 0) { |
| 874 | QPoint p((event->data.data32[2] & 0xffff0000) >> 16, event->data.data32[2] & 0x0000ffff); |
| 875 | QSize s((event->data.data32[3] & 0xffff0000) >> 16, event->data.data32[3] & 0x0000ffff); |
| 876 | source_sameanswer = QRect(p, s); |
| 877 | } else { |
| 878 | source_sameanswer = QRect(); |
| 879 | } |
| 880 | } |
| 881 | |
| 882 | void QXcbDrag::handleStatus(const xcb_client_message_event_t *event) |
| 883 | { |
| 884 | if (event->window != connection()->qtSelectionOwner() || !drag()) |
| 885 | return; |
| 886 | |
| 887 | xcb_client_message_event_t *lastEvent = const_cast<xcb_client_message_event_t *>(event); |
| 888 | xcb_generic_event_t *nextEvent; |
| 889 | ClientMessageScanner scanner(atom(atom: QXcbAtom::AtomXdndStatus)); |
| 890 | while ((nextEvent = connection()->eventQueue()->peek(peeker&: scanner))) { |
| 891 | if (lastEvent != event) |
| 892 | free(ptr: lastEvent); |
| 893 | lastEvent = (xcb_client_message_event_t *)nextEvent; |
| 894 | } |
| 895 | |
| 896 | handle_xdnd_status(event: lastEvent); |
| 897 | if (lastEvent != event) |
| 898 | free(ptr: lastEvent); |
| 899 | } |
| 900 | |
| 901 | void QXcbDrag::handleLeave(QPlatformWindow *w, const xcb_client_message_event_t *event) |
| 902 | { |
| 903 | // If the target receives XdndLeave, it frees any cached data and forgets the whole incident. |
| 904 | qCDebug(lcQpaXDnd) << "target:" << event->window << "received XdndLeave" ; |
| 905 | |
| 906 | if (!currentWindow || w != currentWindow.data()->handle()) { |
| 907 | stopListeningForActionListChanges(); |
| 908 | return; // sanity |
| 909 | } |
| 910 | |
| 911 | // ### |
| 912 | // if (checkEmbedded(current_embedding_widget, event)) { |
| 913 | // current_embedding_widget = 0; |
| 914 | // currentWindow.clear(); |
| 915 | // return; |
| 916 | // } |
| 917 | |
| 918 | if (event->data.data32[0] != xdnd_dragsource) { |
| 919 | // This often happens - leave other-process window quickly |
| 920 | qCDebug(lcQpaXDnd, "xdnd drag leave from unexpected source (%x not %x" , |
| 921 | event->data.data32[0], xdnd_dragsource); |
| 922 | } |
| 923 | |
| 924 | stopListeningForActionListChanges(); |
| 925 | |
| 926 | QWindowSystemInterface::handleDrag(window: w->window(), dropData: nullptr, p: QPoint(), supportedActions: Qt::IgnoreAction, buttons: { }, modifiers: { }); |
| 927 | } |
| 928 | |
| 929 | void QXcbDrag::send_leave() |
| 930 | { |
| 931 | // XdndLeave is sent from the source to the target to cancel the drop. |
| 932 | if (!current_target) |
| 933 | return; |
| 934 | |
| 935 | xcb_client_message_event_t leave; |
| 936 | leave.response_type = XCB_CLIENT_MESSAGE; |
| 937 | leave.sequence = 0; |
| 938 | leave.window = current_target; |
| 939 | leave.format = 32; |
| 940 | leave.type = atom(atom: QXcbAtom::AtomXdndLeave); |
| 941 | leave.data.data32[0] = connection()->qtSelectionOwner(); |
| 942 | leave.data.data32[1] = 0; // flags |
| 943 | leave.data.data32[2] = 0; // x, y |
| 944 | leave.data.data32[3] = 0; // w, h |
| 945 | leave.data.data32[4] = 0; // just null |
| 946 | |
| 947 | QXcbWindow *w = connection()->platformWindowFromId(id: current_proxy_target); |
| 948 | |
| 949 | if (w && (w->window()->type() == Qt::Desktop) /*&& !w->acceptDrops()*/) |
| 950 | w = nullptr; |
| 951 | |
| 952 | qCDebug(lcQpaXDnd) << "sending XdndLeave to target:" << current_target; |
| 953 | |
| 954 | if (w) |
| 955 | handleLeave(w, event: (const xcb_client_message_event_t *)&leave); |
| 956 | else |
| 957 | xcb_send_event(c: xcb_connection(), propagate: false,destination: current_proxy_target, |
| 958 | event_mask: XCB_EVENT_MASK_NO_EVENT, event: (const char *)&leave); |
| 959 | } |
| 960 | |
| 961 | void QXcbDrag::handleDrop(QPlatformWindow *, const xcb_client_message_event_t *event, |
| 962 | Qt::MouseButtons b, Qt::KeyboardModifiers mods) |
| 963 | { |
| 964 | // Target receives XdndDrop. Once it is finished processing the drop, it sends XdndFinished. |
| 965 | qCDebug(lcQpaXDnd) << "target:" << event->window << "received XdndDrop" ; |
| 966 | |
| 967 | if (!currentWindow) { |
| 968 | stopListeningForActionListChanges(); |
| 969 | xdnd_dragsource = 0; |
| 970 | return; // sanity |
| 971 | } |
| 972 | |
| 973 | const uint32_t *l = event->data.data32; |
| 974 | |
| 975 | if (l[0] != xdnd_dragsource) { |
| 976 | qCDebug(lcQpaXDnd, "xdnd drop from unexpected source (%x not %x" , l[0], xdnd_dragsource); |
| 977 | return; |
| 978 | } |
| 979 | |
| 980 | // update the "user time" from the timestamp in the event. |
| 981 | if (l[2] != 0) |
| 982 | target_time = l[2]; |
| 983 | |
| 984 | Qt::DropActions supported_drop_actions; |
| 985 | QMimeData *dropData = nullptr; |
| 986 | // this could be a same-application drop, just proxied due to |
| 987 | // some XEMBEDding, so try to find the real QMimeData used |
| 988 | // based on the timestamp for this drop. |
| 989 | int at = findTransactionByTime(timestamp: target_time); |
| 990 | if (at != -1) { |
| 991 | qCDebug(lcQpaXDnd) << "found one transaction via findTransactionByTime()" ; |
| 992 | dropData = transactions.at(i: at).drag->mimeData(); |
| 993 | // Can't use the source QMimeData if we need the image conversion code from xdndObtainData |
| 994 | if (dropData && dropData->hasImage()) |
| 995 | dropData = 0; |
| 996 | } |
| 997 | // if we can't find it, then use the data in the drag manager |
| 998 | if (currentDrag()) { |
| 999 | if (!dropData) |
| 1000 | dropData = currentDrag()->mimeData(); |
| 1001 | supported_drop_actions = Qt::DropActions(l[4]); |
| 1002 | } else { |
| 1003 | if (!dropData) |
| 1004 | dropData = m_dropData; |
| 1005 | supported_drop_actions = accepted_drop_action | toDropActions(atoms: drop_actions); |
| 1006 | } |
| 1007 | |
| 1008 | if (!dropData) |
| 1009 | return; |
| 1010 | |
| 1011 | auto buttons = currentDrag() ? b : connection()->queryMouseButtons(); |
| 1012 | auto modifiers = currentDrag() ? mods : connection()->keyboard()->queryKeyboardModifiers(); |
| 1013 | |
| 1014 | QPlatformDropQtResponse response = QWindowSystemInterface::handleDrop( |
| 1015 | window: currentWindow.data(), dropData, p: currentPosition, supportedActions: supported_drop_actions, |
| 1016 | buttons, modifiers); |
| 1017 | |
| 1018 | Qt::DropAction acceptedAaction = response.acceptedAction(); |
| 1019 | if (!response.isAccepted()) { |
| 1020 | // Ignore a failed drag |
| 1021 | acceptedAaction = Qt::IgnoreAction; |
| 1022 | } |
| 1023 | setExecutedDropAction(acceptedAaction); |
| 1024 | |
| 1025 | xcb_client_message_event_t finished = {}; |
| 1026 | finished.response_type = XCB_CLIENT_MESSAGE; |
| 1027 | finished.sequence = 0; |
| 1028 | finished.window = xdnd_dragsource; |
| 1029 | finished.format = 32; |
| 1030 | finished.type = atom(atom: QXcbAtom::AtomXdndFinished); |
| 1031 | finished.data.data32[0] = currentWindow ? xcb_window(w: currentWindow.data()) : XCB_NONE; |
| 1032 | finished.data.data32[1] = response.isAccepted(); // flags |
| 1033 | finished.data.data32[2] = toXdndAction(a: acceptedAaction); |
| 1034 | |
| 1035 | qCDebug(lcQpaXDnd) << "sending XdndFinished to source:" << xdnd_dragsource; |
| 1036 | |
| 1037 | xcb_send_event(c: xcb_connection(), propagate: false, destination: current_proxy_target, |
| 1038 | event_mask: XCB_EVENT_MASK_NO_EVENT, event: (char *)&finished); |
| 1039 | |
| 1040 | stopListeningForActionListChanges(); |
| 1041 | |
| 1042 | dropped = true; |
| 1043 | } |
| 1044 | |
| 1045 | void QXcbDrag::handleFinished(const xcb_client_message_event_t *event) |
| 1046 | { |
| 1047 | // Source receives XdndFinished when target is done processing the drop data. |
| 1048 | qCDebug(lcQpaXDnd) << "source:" << event->window << "received XdndFinished" ; |
| 1049 | |
| 1050 | if (event->window != connection()->qtSelectionOwner()) |
| 1051 | return; |
| 1052 | |
| 1053 | if (xcb_window_t w = event->data.data32[0]) { |
| 1054 | int at = findTransactionByWindow(window: w); |
| 1055 | if (at != -1) { |
| 1056 | |
| 1057 | Transaction t = transactions.takeAt(i: at); |
| 1058 | if (t.drag) |
| 1059 | t.drag->deleteLater(); |
| 1060 | // QDragManager *manager = QDragManager::self(); |
| 1061 | |
| 1062 | // Window target = current_target; |
| 1063 | // Window proxy_target = current_proxy_target; |
| 1064 | // QWidget *embedding_widget = current_embedding_widget; |
| 1065 | // QDrag *currentObject = manager->object; |
| 1066 | |
| 1067 | // current_target = t.target; |
| 1068 | // current_proxy_target = t.proxy_target; |
| 1069 | // current_embedding_widget = t.embedding_widget; |
| 1070 | // manager->object = t.object; |
| 1071 | |
| 1072 | // if (!passive) |
| 1073 | // (void) checkEmbedded(currentWindow, xe); |
| 1074 | |
| 1075 | // current_embedding_widget = 0; |
| 1076 | // current_target = 0; |
| 1077 | // current_proxy_target = 0; |
| 1078 | |
| 1079 | // current_target = target; |
| 1080 | // current_proxy_target = proxy_target; |
| 1081 | // current_embedding_widget = embedding_widget; |
| 1082 | // manager->object = currentObject; |
| 1083 | } else { |
| 1084 | qWarning(msg: "QXcbDrag::handleFinished - drop data has expired" ); |
| 1085 | } |
| 1086 | } |
| 1087 | waiting_for_status = false; |
| 1088 | } |
| 1089 | |
| 1090 | void QXcbDrag::timerEvent(QTimerEvent* e) |
| 1091 | { |
| 1092 | if (e->id() == cleanup_timer.id()) { |
| 1093 | bool stopTimer = true; |
| 1094 | for (int i = 0; i < transactions.size(); ++i) { |
| 1095 | const Transaction &t = transactions.at(i); |
| 1096 | if (t.targetWindow) { |
| 1097 | // dnd within the same process, don't delete, these are taken care of |
| 1098 | // in handleFinished() |
| 1099 | continue; |
| 1100 | } |
| 1101 | QTime currentTime = QTime::currentTime(); |
| 1102 | std::chrono::milliseconds delta{t.time.msecsTo(t: currentTime)}; |
| 1103 | if (delta > XdndDropTransactionTimeout) { |
| 1104 | /* delete transactions which are older than XdndDropTransactionTimeout. It could mean |
| 1105 | one of these: |
| 1106 | - client has crashed and as a result we have never received XdndFinished |
| 1107 | - showing dialog box on drop event where user's response takes more time than XdndDropTransactionTimeout (QTBUG-14493) |
| 1108 | - dnd takes unusually long time to process data |
| 1109 | */ |
| 1110 | if (t.drag) |
| 1111 | t.drag->deleteLater(); |
| 1112 | transactions.removeAt(i: i--); |
| 1113 | } else { |
| 1114 | stopTimer = false; |
| 1115 | } |
| 1116 | |
| 1117 | } |
| 1118 | if (stopTimer) |
| 1119 | cleanup_timer.stop(); |
| 1120 | } |
| 1121 | } |
| 1122 | |
| 1123 | void QXcbDrag::cancel() |
| 1124 | { |
| 1125 | qCDebug(lcQpaXDnd) << "dnd was canceled" ; |
| 1126 | |
| 1127 | QBasicDrag::cancel(); |
| 1128 | if (current_target) |
| 1129 | send_leave(); |
| 1130 | |
| 1131 | // remove canceled object |
| 1132 | if (currentDrag()) |
| 1133 | currentDrag()->deleteLater(); |
| 1134 | |
| 1135 | canceled = true; |
| 1136 | } |
| 1137 | |
| 1138 | static xcb_window_t findXdndAwareParent(QXcbConnection *c, xcb_window_t window) |
| 1139 | { |
| 1140 | xcb_window_t target = 0; |
| 1141 | forever { |
| 1142 | // check if window has XdndAware |
| 1143 | auto gpReply = Q_XCB_REPLY(xcb_get_property, c->xcb_connection(), false, window, |
| 1144 | c->atom(QXcbAtom::AtomXdndAware), XCB_GET_PROPERTY_TYPE_ANY, 0, 0); |
| 1145 | bool aware = gpReply && gpReply->type != XCB_NONE; |
| 1146 | if (aware) { |
| 1147 | target = window; |
| 1148 | break; |
| 1149 | } |
| 1150 | |
| 1151 | // try window's parent |
| 1152 | auto qtReply = Q_XCB_REPLY_UNCHECKED(xcb_query_tree, c->xcb_connection(), window); |
| 1153 | if (!qtReply) |
| 1154 | break; |
| 1155 | xcb_window_t root = qtReply->root; |
| 1156 | xcb_window_t parent = qtReply->parent; |
| 1157 | if (window == root) |
| 1158 | break; |
| 1159 | window = parent; |
| 1160 | } |
| 1161 | return target; |
| 1162 | } |
| 1163 | |
| 1164 | void QXcbDrag::handleSelectionRequest(const xcb_selection_request_event_t *event) |
| 1165 | { |
| 1166 | qCDebug(lcQpaXDnd) << "handle selection request from target:" << event->requestor; |
| 1167 | q_padded_xcb_event<xcb_selection_notify_event_t> notify = {}; |
| 1168 | notify.response_type = XCB_SELECTION_NOTIFY; |
| 1169 | notify.requestor = event->requestor; |
| 1170 | notify.selection = event->selection; |
| 1171 | notify.target = XCB_NONE; |
| 1172 | notify.property = XCB_NONE; |
| 1173 | notify.time = event->time; |
| 1174 | |
| 1175 | // which transaction do we use? (note: -2 means use current currentDrag()) |
| 1176 | int at = -1; |
| 1177 | |
| 1178 | // figure out which data the requestor is really interested in |
| 1179 | if (currentDrag() && event->time == source_time) { |
| 1180 | // requestor wants the current drag data |
| 1181 | at = -2; |
| 1182 | } else { |
| 1183 | // if someone has requested data in response to XdndDrop, find the corresponding transaction. the |
| 1184 | // spec says to call xcb_convert_selection() using the timestamp from the XdndDrop |
| 1185 | at = findTransactionByTime(timestamp: event->time); |
| 1186 | if (at == -1) { |
| 1187 | // no dice, perhaps the client was nice enough to use the same window id in |
| 1188 | // xcb_convert_selection() that we sent the XdndDrop event to. |
| 1189 | at = findTransactionByWindow(window: event->requestor); |
| 1190 | } |
| 1191 | |
| 1192 | if (at == -1) { |
| 1193 | xcb_window_t target = findXdndAwareParent(c: connection(), window: event->requestor); |
| 1194 | if (target) { |
| 1195 | if (event->time == XCB_CURRENT_TIME && current_target == target) |
| 1196 | at = -2; |
| 1197 | else |
| 1198 | at = findTransactionByWindow(window: target); |
| 1199 | } |
| 1200 | } |
| 1201 | } |
| 1202 | |
| 1203 | QDrag *transactionDrag = nullptr; |
| 1204 | if (at >= 0) { |
| 1205 | transactionDrag = transactions.at(i: at).drag; |
| 1206 | } else if (at == -2) { |
| 1207 | transactionDrag = currentDrag(); |
| 1208 | } |
| 1209 | |
| 1210 | if (transactionDrag) { |
| 1211 | xcb_atom_t atomFormat = event->target; |
| 1212 | int dataFormat = 0; |
| 1213 | QByteArray data; |
| 1214 | if (QXcbMime::mimeDataForAtom(connection: connection(), a: event->target, mimeData: transactionDrag->mimeData(), |
| 1215 | data: &data, atomFormat: &atomFormat, dataFormat: &dataFormat)) { |
| 1216 | int dataSize = data.size() / (dataFormat / 8); |
| 1217 | xcb_change_property(c: xcb_connection(), mode: XCB_PROP_MODE_REPLACE, window: event->requestor, property: event->property, |
| 1218 | type: atomFormat, format: dataFormat, data_len: dataSize, data: (const void *)data.constData()); |
| 1219 | notify.property = event->property; |
| 1220 | notify.target = atomFormat; |
| 1221 | } |
| 1222 | } |
| 1223 | |
| 1224 | xcb_window_t proxy_target = xdndProxy(c: connection(), w: event->requestor); |
| 1225 | if (!proxy_target) |
| 1226 | proxy_target = event->requestor; |
| 1227 | |
| 1228 | xcb_send_event(c: xcb_connection(), propagate: false, destination: proxy_target, event_mask: XCB_EVENT_MASK_NO_EVENT, event: (const char *)¬ify); |
| 1229 | } |
| 1230 | |
| 1231 | |
| 1232 | bool QXcbDrag::dndEnable(QXcbWindow *w, bool on) |
| 1233 | { |
| 1234 | qCDebug(lcQpaXDnd) << "dndEnable" << static_cast<QPlatformWindow *>(w) << on; |
| 1235 | // Windows announce that they support the XDND protocol by creating a window property XdndAware. |
| 1236 | if (on) { |
| 1237 | QXcbWindow *window = nullptr; |
| 1238 | if (w->window()->type() == Qt::Desktop) { |
| 1239 | if (desktop_proxy) // *WE* already have one. |
| 1240 | return false; |
| 1241 | |
| 1242 | QXcbConnectionGrabber grabber(connection()); |
| 1243 | |
| 1244 | // As per Xdnd4, use XdndProxy |
| 1245 | xcb_window_t proxy_id = xdndProxy(c: connection(), w: w->xcb_window()); |
| 1246 | |
| 1247 | if (!proxy_id) { |
| 1248 | desktop_proxy = new QWindow; |
| 1249 | window = static_cast<QXcbWindow *>(desktop_proxy->handle()); |
| 1250 | proxy_id = window->xcb_window(); |
| 1251 | xcb_atom_t xdnd_proxy = atom(atom: QXcbAtom::AtomXdndProxy); |
| 1252 | xcb_change_property(c: xcb_connection(), mode: XCB_PROP_MODE_REPLACE, window: w->xcb_window(), property: xdnd_proxy, |
| 1253 | type: XCB_ATOM_WINDOW, format: 32, data_len: 1, data: &proxy_id); |
| 1254 | xcb_change_property(c: xcb_connection(), mode: XCB_PROP_MODE_REPLACE, window: proxy_id, property: xdnd_proxy, |
| 1255 | type: XCB_ATOM_WINDOW, format: 32, data_len: 1, data: &proxy_id); |
| 1256 | } |
| 1257 | |
| 1258 | } else { |
| 1259 | window = w; |
| 1260 | } |
| 1261 | if (window) { |
| 1262 | qCDebug(lcQpaXDnd) << "setting XdndAware for" << window->xcb_window(); |
| 1263 | xcb_atom_t atm = xdnd_version; |
| 1264 | xcb_change_property(c: xcb_connection(), mode: XCB_PROP_MODE_REPLACE, window: window->xcb_window(), |
| 1265 | property: atom(atom: QXcbAtom::AtomXdndAware), type: XCB_ATOM_ATOM, format: 32, data_len: 1, data: &atm); |
| 1266 | return true; |
| 1267 | } else { |
| 1268 | return false; |
| 1269 | } |
| 1270 | } else { |
| 1271 | if (w->window()->type() == Qt::Desktop) { |
| 1272 | xcb_delete_property(c: xcb_connection(), window: w->xcb_window(), property: atom(atom: QXcbAtom::AtomXdndProxy)); |
| 1273 | delete desktop_proxy; |
| 1274 | desktop_proxy = nullptr; |
| 1275 | } else { |
| 1276 | qCDebug(lcQpaXDnd) << "not deleting XDndAware" ; |
| 1277 | } |
| 1278 | return true; |
| 1279 | } |
| 1280 | } |
| 1281 | |
| 1282 | bool QXcbDrag::ownsDragObject() const |
| 1283 | { |
| 1284 | return true; |
| 1285 | } |
| 1286 | |
| 1287 | QXcbDropData::QXcbDropData(QXcbDrag *d) |
| 1288 | : QXcbMime(), |
| 1289 | drag(d) |
| 1290 | { |
| 1291 | } |
| 1292 | |
| 1293 | QXcbDropData::~QXcbDropData() |
| 1294 | { |
| 1295 | } |
| 1296 | |
| 1297 | QVariant QXcbDropData::retrieveData_sys(const QString &mimetype, QMetaType requestedType) const |
| 1298 | { |
| 1299 | QByteArray mime = mimetype.toLatin1(); |
| 1300 | QVariant data = xdndObtainData(format: mime, requestedType); |
| 1301 | return data; |
| 1302 | } |
| 1303 | |
| 1304 | QVariant QXcbDropData::xdndObtainData(const QByteArray &format, QMetaType requestedType) const |
| 1305 | { |
| 1306 | QXcbConnection *c = drag->connection(); |
| 1307 | QXcbWindow *xcb_window = c->platformWindowFromId(id: drag->xdnd_dragsource); |
| 1308 | if (xcb_window && drag->currentDrag() && xcb_window->window()->type() != Qt::Desktop) { |
| 1309 | QMimeData *data = drag->currentDrag()->mimeData(); |
| 1310 | if (data->hasFormat(mimetype: QLatin1StringView(format))) |
| 1311 | return data->data(mimetype: QLatin1StringView(format)); |
| 1312 | return QVariant(); |
| 1313 | } |
| 1314 | |
| 1315 | QList<xcb_atom_t> atoms = drag->xdnd_types; |
| 1316 | bool hasUtf8 = false; |
| 1317 | xcb_atom_t a = mimeAtomForFormat(connection: c, format: QLatin1StringView(format), requestedType, atoms, hasUtf8: &hasUtf8); |
| 1318 | if (a == XCB_NONE) |
| 1319 | return QVariant(); |
| 1320 | |
| 1321 | #ifndef QT_NO_CLIPBOARD |
| 1322 | if (c->selectionOwner(atom: c->atom(qatom: QXcbAtom::AtomXdndSelection)) == XCB_NONE) |
| 1323 | return QVariant(); // should never happen? |
| 1324 | |
| 1325 | xcb_atom_t xdnd_selection = c->atom(qatom: QXcbAtom::AtomXdndSelection); |
| 1326 | const std::optional<QByteArray> result = c->clipboard()->getSelection(selection: xdnd_selection, target: a, property: xdnd_selection, t: drag->targetTime()); |
| 1327 | if (!result.has_value()) |
| 1328 | return QVariant(); |
| 1329 | return mimeConvertToFormat(connection: c, a, data: result.value(), format: QLatin1StringView(format), requestedType, hasUtf8); |
| 1330 | #else |
| 1331 | return QVariant(); |
| 1332 | #endif |
| 1333 | } |
| 1334 | |
| 1335 | bool QXcbDropData::hasFormat_sys(const QString &format) const |
| 1336 | { |
| 1337 | return formats().contains(str: format); |
| 1338 | } |
| 1339 | |
| 1340 | QStringList QXcbDropData::formats_sys() const |
| 1341 | { |
| 1342 | QStringList formats; |
| 1343 | for (int i = 0; i < drag->xdnd_types.size(); ++i) { |
| 1344 | QString f = mimeAtomToString(connection: drag->connection(), a: drag->xdnd_types.at(i)); |
| 1345 | if (!formats.contains(str: f)) |
| 1346 | formats.append(t: f); |
| 1347 | } |
| 1348 | return formats; |
| 1349 | } |
| 1350 | |
| 1351 | QT_END_NAMESPACE |
| 1352 | |