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 "qsimpledrag_p.h" |
5 | |
6 | #include "qbitmap.h" |
7 | #include "qdrag.h" |
8 | #include "qpixmap.h" |
9 | #include "qevent.h" |
10 | #include "qfile.h" |
11 | #include "qguiapplication.h" |
12 | #include "qpoint.h" |
13 | #include "qbuffer.h" |
14 | #include "qimage.h" |
15 | #include "qdir.h" |
16 | #include "qimagereader.h" |
17 | #include "qimagewriter.h" |
18 | #include "qplatformscreen.h" |
19 | #include "qplatformwindow.h" |
20 | |
21 | #include <QtCore/QEventLoop> |
22 | #include <QtCore/QDebug> |
23 | #include <QtCore/QLoggingCategory> |
24 | |
25 | #include <private/qguiapplication_p.h> |
26 | #include <private/qdnd_p.h> |
27 | |
28 | #include <private/qshapedpixmapdndwindow_p.h> |
29 | #include <private/qhighdpiscaling_p.h> |
30 | |
31 | QT_BEGIN_NAMESPACE |
32 | |
33 | Q_LOGGING_CATEGORY(lcDnd, "qt.gui.dnd" ) |
34 | |
35 | static QWindow* topLevelAt(const QPoint &pos) |
36 | { |
37 | const QWindowList list = QGuiApplication::topLevelWindows(); |
38 | const auto crend = list.crend(); |
39 | for (auto it = list.crbegin(); it != crend; ++it) { |
40 | QWindow *w = *it; |
41 | if (w->isVisible() && w->handle() && w->geometry().contains(p: pos) && !qobject_cast<QShapedPixmapWindow*>(object: w)) |
42 | return w; |
43 | } |
44 | return nullptr; |
45 | } |
46 | |
47 | /*! |
48 | \class QBasicDrag |
49 | \brief QBasicDrag is a base class for implementing platform drag and drop. |
50 | \since 5.0 |
51 | \internal |
52 | \ingroup qpa |
53 | |
54 | QBasicDrag implements QPlatformDrag::drag() by running a local event loop in which |
55 | it tracks mouse movements and moves the drag icon (QShapedPixmapWindow) accordingly. |
56 | It provides new virtuals allowing for querying whether the receiving window |
57 | (within the Qt application or outside) accepts the drag and sets the state accordingly. |
58 | */ |
59 | |
60 | QBasicDrag::QBasicDrag() |
61 | { |
62 | } |
63 | |
64 | QBasicDrag::~QBasicDrag() |
65 | { |
66 | delete m_drag_icon_window; |
67 | } |
68 | |
69 | void QBasicDrag::enableEventFilter() |
70 | { |
71 | qApp->installEventFilter(filterObj: this); |
72 | } |
73 | |
74 | void QBasicDrag::disableEventFilter() |
75 | { |
76 | qApp->removeEventFilter(obj: this); |
77 | } |
78 | |
79 | |
80 | static inline QPoint getNativeMousePos(QEvent *e, QWindow *window) |
81 | { |
82 | return QHighDpi::toNativePixels(value: static_cast<QMouseEvent *>(e)->globalPosition().toPoint(), context: window); |
83 | } |
84 | |
85 | bool QBasicDrag::eventFilter(QObject *o, QEvent *e) |
86 | { |
87 | Q_UNUSED(o); |
88 | |
89 | if (!m_drag) { |
90 | if (e->type() == QEvent::KeyRelease && static_cast<QKeyEvent*>(e)->key() == Qt::Key_Escape) { |
91 | disableEventFilter(); |
92 | exitDndEventLoop(); |
93 | return true; // block the key release |
94 | } |
95 | return false; |
96 | } |
97 | |
98 | switch (e->type()) { |
99 | case QEvent::ShortcutOverride: |
100 | // prevent accelerators from firing while dragging |
101 | e->accept(); |
102 | return true; |
103 | |
104 | case QEvent::KeyPress: |
105 | case QEvent::KeyRelease: |
106 | { |
107 | QKeyEvent *ke = static_cast<QKeyEvent *>(e); |
108 | if (ke->key() == Qt::Key_Escape && e->type() == QEvent::KeyPress) { |
109 | cancel(); |
110 | disableEventFilter(); |
111 | exitDndEventLoop(); |
112 | |
113 | } else if (ke->modifiers() != QGuiApplication::keyboardModifiers()) { |
114 | move(globalPos: m_lastPos, b: QGuiApplication::mouseButtons(), mods: ke->modifiers()); |
115 | } |
116 | return true; // Eat all key events |
117 | } |
118 | |
119 | case QEvent::MouseMove: |
120 | { |
121 | m_lastPos = getNativeMousePos(e, window: m_drag_icon_window); |
122 | auto mouseMove = static_cast<QMouseEvent *>(e); |
123 | move(globalPos: m_lastPos, b: mouseMove->buttons(), mods: mouseMove->modifiers()); |
124 | return true; // Eat all mouse move events |
125 | } |
126 | case QEvent::MouseButtonRelease: |
127 | { |
128 | QPointer<QObject> objGuard(o); |
129 | disableEventFilter(); |
130 | if (canDrop()) { |
131 | QPoint nativePosition = getNativeMousePos(e, window: m_drag_icon_window); |
132 | auto mouseRelease = static_cast<QMouseEvent *>(e); |
133 | drop(globalPos: nativePosition, b: mouseRelease->buttons(), mods: mouseRelease->modifiers()); |
134 | } else { |
135 | cancel(); |
136 | } |
137 | exitDndEventLoop(); |
138 | if (!objGuard) |
139 | return true; |
140 | |
141 | // If a QShapedPixmapWindow (drag feedback) is being dragged along, the |
142 | // mouse event's localPos() will be relative to that, which is useless. |
143 | // We want a position relative to the window where the drag ends, if possible (?). |
144 | // If there is no such window (belonging to this Qt application), |
145 | // make the event relative to the window where the drag started. (QTBUG-66103) |
146 | const QMouseEvent *release = static_cast<QMouseEvent *>(e); |
147 | const QWindow *releaseWindow = topLevelAt(pos: release->globalPosition().toPoint()); |
148 | qCDebug(lcDnd) << "mouse released over" << releaseWindow << "after drag from" << m_sourceWindow << "globalPos" << release->globalPosition().toPoint(); |
149 | if (!releaseWindow) |
150 | releaseWindow = m_sourceWindow; |
151 | QPointF releaseWindowPos = (releaseWindow ? releaseWindow->mapFromGlobal(pos: release->globalPosition()) : release->globalPosition()); |
152 | QMouseEvent *newRelease = new QMouseEvent(release->type(), |
153 | releaseWindowPos, releaseWindowPos, release->globalPosition(), |
154 | release->button(), release->buttons(), |
155 | release->modifiers(), release->source(), release->pointingDevice()); |
156 | QCoreApplication::postEvent(receiver: o, event: newRelease); |
157 | return true; // defer mouse release events until drag event loop has returned |
158 | } |
159 | case QEvent::MouseButtonDblClick: |
160 | case QEvent::Wheel: |
161 | return true; |
162 | default: |
163 | break; |
164 | } |
165 | return false; |
166 | } |
167 | |
168 | Qt::DropAction QBasicDrag::drag(QDrag *o) |
169 | { |
170 | m_drag = o; |
171 | m_executed_drop_action = Qt::IgnoreAction; |
172 | m_can_drop = false; |
173 | |
174 | startDrag(); |
175 | m_eventLoop = new QEventLoop; |
176 | m_eventLoop->exec(); |
177 | delete m_eventLoop; |
178 | m_eventLoop = nullptr; |
179 | m_drag = nullptr; |
180 | endDrag(); |
181 | |
182 | return m_executed_drop_action; |
183 | } |
184 | |
185 | void QBasicDrag::cancelDrag() |
186 | { |
187 | if (m_eventLoop) { |
188 | cancel(); |
189 | m_eventLoop->quit(); |
190 | } |
191 | } |
192 | |
193 | void QBasicDrag::startDrag() |
194 | { |
195 | QPoint pos; |
196 | #ifndef QT_NO_CURSOR |
197 | pos = QCursor::pos(); |
198 | if (pos.x() == int(qInf())) { |
199 | // ### fixme: no mouse pos registered. Get pos from touch... |
200 | pos = QPoint(); |
201 | } |
202 | #endif |
203 | m_lastPos = pos; |
204 | recreateShapedPixmapWindow(screen: m_screen, pos); |
205 | enableEventFilter(); |
206 | } |
207 | |
208 | void QBasicDrag::endDrag() |
209 | { |
210 | } |
211 | |
212 | void QBasicDrag::recreateShapedPixmapWindow(QScreen *screen, const QPoint &pos) |
213 | { |
214 | delete m_drag_icon_window; |
215 | // ### TODO Check if its really necessary to have m_drag_icon_window |
216 | // when QDrag is used without a pixmap - QDrag::setPixmap() |
217 | m_drag_icon_window = new QShapedPixmapWindow(screen); |
218 | |
219 | m_drag_icon_window->setUseCompositing(m_useCompositing); |
220 | m_drag_icon_window->setPixmap(m_drag->pixmap()); |
221 | m_drag_icon_window->setHotspot(m_drag->hotSpot()); |
222 | m_drag_icon_window->updateGeometry(pos); |
223 | m_drag_icon_window->setVisible(true); |
224 | } |
225 | |
226 | void QBasicDrag::cancel() |
227 | { |
228 | disableEventFilter(); |
229 | restoreCursor(); |
230 | m_drag_icon_window->setVisible(false); |
231 | } |
232 | |
233 | /*! |
234 | Move the drag label to \a globalPos, which is |
235 | interpreted in device independent coordinates. Typically called from reimplementations of move(). |
236 | */ |
237 | |
238 | void QBasicDrag::moveShapedPixmapWindow(const QPoint &globalPos) |
239 | { |
240 | if (m_drag) |
241 | m_drag_icon_window->updateGeometry(pos: globalPos); |
242 | } |
243 | |
244 | void QBasicDrag::drop(const QPoint &, Qt::MouseButtons, Qt::KeyboardModifiers) |
245 | { |
246 | disableEventFilter(); |
247 | restoreCursor(); |
248 | m_drag_icon_window->setVisible(false); |
249 | } |
250 | |
251 | void QBasicDrag::exitDndEventLoop() |
252 | { |
253 | if (m_eventLoop && m_eventLoop->isRunning()) |
254 | m_eventLoop->exit(); |
255 | } |
256 | |
257 | void QBasicDrag::updateCursor(Qt::DropAction action) |
258 | { |
259 | #ifndef QT_NO_CURSOR |
260 | Qt::CursorShape cursorShape = Qt::ForbiddenCursor; |
261 | if (canDrop()) { |
262 | switch (action) { |
263 | case Qt::CopyAction: |
264 | cursorShape = Qt::DragCopyCursor; |
265 | break; |
266 | case Qt::LinkAction: |
267 | cursorShape = Qt::DragLinkCursor; |
268 | break; |
269 | default: |
270 | cursorShape = Qt::DragMoveCursor; |
271 | break; |
272 | } |
273 | } |
274 | |
275 | QPixmap pixmap = m_drag->dragCursor(action); |
276 | |
277 | if (!m_dndHasSetOverrideCursor) { |
278 | QCursor newCursor = !pixmap.isNull() ? QCursor(pixmap) : QCursor(cursorShape); |
279 | QGuiApplication::setOverrideCursor(newCursor); |
280 | m_dndHasSetOverrideCursor = true; |
281 | } else { |
282 | QCursor *cursor = QGuiApplication::overrideCursor(); |
283 | if (!cursor) { |
284 | QGuiApplication::changeOverrideCursor(pixmap.isNull() ? QCursor(cursorShape) : QCursor(pixmap)); |
285 | } else { |
286 | if (!pixmap.isNull()) { |
287 | if (cursor->pixmap().cacheKey() != pixmap.cacheKey()) |
288 | QGuiApplication::changeOverrideCursor(QCursor(pixmap)); |
289 | } else if (cursorShape != cursor->shape()) { |
290 | QGuiApplication::changeOverrideCursor(QCursor(cursorShape)); |
291 | } |
292 | } |
293 | } |
294 | #endif |
295 | updateAction(action); |
296 | } |
297 | |
298 | void QBasicDrag::restoreCursor() |
299 | { |
300 | #ifndef QT_NO_CURSOR |
301 | if (m_dndHasSetOverrideCursor) { |
302 | QGuiApplication::restoreOverrideCursor(); |
303 | m_dndHasSetOverrideCursor = false; |
304 | } |
305 | #endif |
306 | } |
307 | |
308 | static inline QPoint fromNativeGlobalPixels(const QPoint &point) |
309 | { |
310 | #ifndef QT_NO_HIGHDPISCALING |
311 | QPoint res = point; |
312 | if (QHighDpiScaling::isActive()) { |
313 | for (const QScreen *s : std::as_const(t&: QGuiApplicationPrivate::screen_list)) { |
314 | if (s->handle()->geometry().contains(p: point)) { |
315 | res = QHighDpi::fromNativePixels(value: point, context: s); |
316 | break; |
317 | } |
318 | } |
319 | } |
320 | return res; |
321 | #else |
322 | return point; |
323 | #endif |
324 | } |
325 | |
326 | /*! |
327 | \class QSimpleDrag |
328 | \brief QSimpleDrag implements QBasicDrag for Drag and Drop operations within the Qt Application itself. |
329 | \since 5.0 |
330 | \internal |
331 | \ingroup qpa |
332 | |
333 | The class checks whether the receiving window is a window of the Qt application |
334 | and sets the state accordingly. It does not take windows of other applications |
335 | into account. |
336 | */ |
337 | |
338 | QSimpleDrag::QSimpleDrag() |
339 | { |
340 | } |
341 | |
342 | void QSimpleDrag::startDrag() |
343 | { |
344 | setExecutedDropAction(Qt::IgnoreAction); |
345 | |
346 | QBasicDrag::startDrag(); |
347 | // Here we can be fairly sure that QGuiApplication::mouseButtons/keyboardModifiers() will |
348 | // contain sensible values as startDrag() normally is called from mouse event handlers |
349 | // by QDrag::exec(). A better API would be if we could pass something like "input device |
350 | // pointer" to QDrag::exec(). My guess is that something like that might be required for |
351 | // QTBUG-52430. |
352 | m_sourceWindow = topLevelAt(pos: QCursor::pos()); |
353 | m_windowUnderCursor = m_sourceWindow; |
354 | if (m_sourceWindow) { |
355 | auto nativePixelPos = QHighDpi::toNativePixels(value: QCursor::pos(), context: m_sourceWindow); |
356 | move(globalPos: nativePixelPos, b: QGuiApplication::mouseButtons(), mods: QGuiApplication::keyboardModifiers()); |
357 | } else { |
358 | setCanDrop(false); |
359 | updateCursor(action: Qt::IgnoreAction); |
360 | } |
361 | |
362 | qCDebug(lcDnd) << "drag began from" << m_sourceWindow << "cursor pos" << QCursor::pos() << "can drop?" << canDrop(); |
363 | } |
364 | |
365 | static void sendDragLeave(QWindow *window) |
366 | { |
367 | QWindowSystemInterface::handleDrag(window, dropData: nullptr, p: QPoint(), supportedActions: Qt::IgnoreAction, buttons: { }, modifiers: { }); |
368 | } |
369 | |
370 | void QSimpleDrag::cancel() |
371 | { |
372 | QBasicDrag::cancel(); |
373 | if (drag() && m_sourceWindow) { |
374 | sendDragLeave(window: m_sourceWindow); |
375 | m_sourceWindow = nullptr; |
376 | } |
377 | } |
378 | |
379 | void QSimpleDrag::move(const QPoint &nativeGlobalPos, Qt::MouseButtons buttons, |
380 | Qt::KeyboardModifiers modifiers) |
381 | { |
382 | QPoint globalPos = fromNativeGlobalPixels(point: nativeGlobalPos); |
383 | moveShapedPixmapWindow(globalPos); |
384 | QWindow *window = topLevelAt(pos: globalPos); |
385 | |
386 | if (!window || window != m_windowUnderCursor) { |
387 | if (m_windowUnderCursor) |
388 | sendDragLeave(window: m_windowUnderCursor); |
389 | m_windowUnderCursor = window; |
390 | if (!window) { |
391 | // QSimpleDrag supports only in-process dnd, we can't drop anywhere else. |
392 | setCanDrop(false); |
393 | updateCursor(action: Qt::IgnoreAction); |
394 | return; |
395 | } |
396 | } |
397 | |
398 | const QPoint pos = nativeGlobalPos - window->handle()->geometry().topLeft(); |
399 | const QPlatformDragQtResponse qt_response = QWindowSystemInterface::handleDrag( |
400 | window, dropData: drag()->mimeData(), p: pos, supportedActions: drag()->supportedActions(), |
401 | buttons, modifiers); |
402 | |
403 | setCanDrop(qt_response.isAccepted()); |
404 | updateCursor(action: qt_response.acceptedAction()); |
405 | } |
406 | |
407 | void QSimpleDrag::drop(const QPoint &nativeGlobalPos, Qt::MouseButtons buttons, |
408 | Qt::KeyboardModifiers modifiers) |
409 | { |
410 | QPoint globalPos = fromNativeGlobalPixels(point: nativeGlobalPos); |
411 | |
412 | QBasicDrag::drop(nativeGlobalPos, buttons, modifiers); |
413 | QWindow *window = topLevelAt(pos: globalPos); |
414 | if (!window) |
415 | return; |
416 | |
417 | const QPoint pos = nativeGlobalPos - window->handle()->geometry().topLeft(); |
418 | const QPlatformDropQtResponse response = QWindowSystemInterface::handleDrop( |
419 | window, dropData: drag()->mimeData(), p: pos, supportedActions: drag()->supportedActions(), |
420 | buttons, modifiers); |
421 | if (response.isAccepted()) { |
422 | setExecutedDropAction(response.acceptedAction()); |
423 | } else { |
424 | setExecutedDropAction(Qt::IgnoreAction); |
425 | } |
426 | } |
427 | |
428 | QT_END_NAMESPACE |
429 | |