1 | /* |
2 | SPDX-FileCopyrightText: 2010 BetterInbox <contact@betterinbox.com> |
3 | SPDX-FileContributor: Gregory Schlomoff <greg@betterinbox.com> |
4 | |
5 | SPDX-License-Identifier: MIT |
6 | */ |
7 | |
8 | #include "DeclarativeDragArea.h" |
9 | |
10 | #include <QDrag> |
11 | #include <QGuiApplication> |
12 | #include <QIcon> |
13 | #include <QMimeData> |
14 | #include <QMouseEvent> |
15 | #include <QPainter> |
16 | #include <QQuickItemGrabResult> |
17 | #include <QQuickWindow> |
18 | #include <QStyleHints> |
19 | |
20 | #include <QDebug> |
21 | |
22 | /*! |
23 | A DragArea is used to make an item draggable. |
24 | */ |
25 | |
26 | DeclarativeDragArea::DeclarativeDragArea(QQuickItem *parent) |
27 | : QQuickItem(parent) |
28 | , m_delegate(nullptr) |
29 | , m_source(parent) |
30 | , m_target(nullptr) |
31 | , m_enabled(true) |
32 | , m_draggingJustStarted(false) |
33 | , m_dragActive(false) |
34 | , m_supportedActions(Qt::MoveAction) |
35 | , m_defaultAction(Qt::MoveAction) |
36 | , m_data(new DeclarativeMimeData()) // m_data is owned by us, and we shouldn't pass it to Qt directly |
37 | // as it will automatically delete it after the drag and drop. |
38 | , m_pressAndHoldTimerId(0) |
39 | { |
40 | m_startDragDistance = QGuiApplication::styleHints()->startDragDistance(); |
41 | setAcceptedMouseButtons(Qt::LeftButton); |
42 | // setFiltersChildEvents(true); |
43 | setFlag(flag: ItemAcceptsDrops, enabled: m_enabled); |
44 | setFiltersChildMouseEvents(true); |
45 | } |
46 | |
47 | DeclarativeDragArea::~DeclarativeDragArea() |
48 | { |
49 | if (m_data) { |
50 | delete m_data; |
51 | } |
52 | } |
53 | |
54 | /*! |
55 | The delegate is the item that will be displayed next to the mouse cursor during the drag and drop operation. |
56 | It usually consists of a large, semi-transparent icon representing the data being dragged. |
57 | */ |
58 | QQuickItem *DeclarativeDragArea::delegate() const |
59 | { |
60 | return m_delegate; |
61 | } |
62 | |
63 | void DeclarativeDragArea::setDelegate(QQuickItem *delegate) |
64 | { |
65 | if (m_delegate != delegate) { |
66 | // qDebug() << " ______________________________________________ " << delegate; |
67 | m_delegate = delegate; |
68 | Q_EMIT delegateChanged(); |
69 | } |
70 | } |
71 | void DeclarativeDragArea::resetDelegate() |
72 | { |
73 | setDelegate(nullptr); |
74 | } |
75 | |
76 | /*! |
77 | The QML element that is the source of this drag and drop operation. This can be defined to any item, and will |
78 | be available to the DropArea as event.data.source |
79 | */ |
80 | QQuickItem *DeclarativeDragArea::source() const |
81 | { |
82 | return m_source; |
83 | } |
84 | |
85 | void DeclarativeDragArea::setSource(QQuickItem *source) |
86 | { |
87 | if (m_source != source) { |
88 | m_source = source; |
89 | Q_EMIT sourceChanged(); |
90 | } |
91 | } |
92 | |
93 | void DeclarativeDragArea::resetSource() |
94 | { |
95 | setSource(nullptr); |
96 | } |
97 | |
98 | bool DeclarativeDragArea::dragActive() const |
99 | { |
100 | return m_dragActive; |
101 | } |
102 | |
103 | // target |
104 | QQuickItem *DeclarativeDragArea::target() const |
105 | { |
106 | // TODO: implement me |
107 | return nullptr; |
108 | } |
109 | |
110 | // data |
111 | DeclarativeMimeData *DeclarativeDragArea::mimeData() const |
112 | { |
113 | return m_data; |
114 | } |
115 | |
116 | // startDragDistance |
117 | int DeclarativeDragArea::startDragDistance() const |
118 | { |
119 | return m_startDragDistance; |
120 | } |
121 | |
122 | void DeclarativeDragArea::setStartDragDistance(int distance) |
123 | { |
124 | if (distance == m_startDragDistance) { |
125 | return; |
126 | } |
127 | |
128 | m_startDragDistance = distance; |
129 | Q_EMIT startDragDistanceChanged(); |
130 | } |
131 | |
132 | // delegateImage |
133 | QVariant DeclarativeDragArea::delegateImage() const |
134 | { |
135 | return m_delegateImage; |
136 | } |
137 | |
138 | void DeclarativeDragArea::setDelegateImage(const QVariant &image) |
139 | { |
140 | if (image.canConvert<QImage>() && image.value<QImage>() == m_delegateImage) { |
141 | return; |
142 | } |
143 | |
144 | if (image.canConvert<QImage>()) { |
145 | m_delegateImage = image.value<QImage>(); |
146 | } else if (image.canConvert<QString>()) { |
147 | m_delegateImage = QIcon::fromTheme(name: image.toString()).pixmap(size: QSize(48, 48)).toImage(); |
148 | } else { |
149 | m_delegateImage = image.value<QIcon>().pixmap(size: QSize(48, 48)).toImage(); |
150 | } |
151 | |
152 | Q_EMIT delegateImageChanged(); |
153 | } |
154 | |
155 | // enabled |
156 | bool DeclarativeDragArea::isEnabled() const |
157 | { |
158 | return m_enabled; |
159 | } |
160 | void DeclarativeDragArea::setEnabled(bool enabled) |
161 | { |
162 | if (enabled != m_enabled) { |
163 | m_enabled = enabled; |
164 | Q_EMIT enabledChanged(); |
165 | } |
166 | } |
167 | |
168 | // supported actions |
169 | Qt::DropActions DeclarativeDragArea::supportedActions() const |
170 | { |
171 | return m_supportedActions; |
172 | } |
173 | void DeclarativeDragArea::setSupportedActions(Qt::DropActions actions) |
174 | { |
175 | if (actions != m_supportedActions) { |
176 | m_supportedActions = actions; |
177 | Q_EMIT supportedActionsChanged(); |
178 | } |
179 | } |
180 | |
181 | // default action |
182 | Qt::DropAction DeclarativeDragArea::defaultAction() const |
183 | { |
184 | return m_defaultAction; |
185 | } |
186 | void DeclarativeDragArea::setDefaultAction(Qt::DropAction action) |
187 | { |
188 | if (action != m_defaultAction) { |
189 | m_defaultAction = action; |
190 | Q_EMIT defaultActionChanged(); |
191 | } |
192 | } |
193 | |
194 | void DeclarativeDragArea::mousePressEvent(QMouseEvent *event) |
195 | { |
196 | m_pressAndHoldTimerId = startTimer(interval: QGuiApplication::styleHints()->mousePressAndHoldInterval()); |
197 | m_buttonDownPos = event->globalPosition(); |
198 | m_draggingJustStarted = true; |
199 | setKeepMouseGrab(true); |
200 | } |
201 | |
202 | void DeclarativeDragArea::mouseReleaseEvent(QMouseEvent *event) |
203 | { |
204 | Q_UNUSED(event); |
205 | killTimer(id: m_pressAndHoldTimerId); |
206 | m_pressAndHoldTimerId = 0; |
207 | m_draggingJustStarted = false; |
208 | setKeepMouseGrab(false); |
209 | ungrabMouse(); |
210 | } |
211 | |
212 | void DeclarativeDragArea::timerEvent(QTimerEvent *event) |
213 | { |
214 | if (event->timerId() == m_pressAndHoldTimerId && m_draggingJustStarted && m_enabled) { |
215 | // Grab delegate before starting drag |
216 | if (m_delegate) { |
217 | // Another grab is already in progress |
218 | if (m_grabResult) { |
219 | return; |
220 | } |
221 | m_grabResult = m_delegate->grabToImage(); |
222 | if (m_grabResult) { |
223 | connect(sender: m_grabResult.data(), signal: &QQuickItemGrabResult::ready, context: this, slot: [this]() { |
224 | startDrag(image: m_grabResult->image()); |
225 | m_grabResult.reset(); |
226 | }); |
227 | return; |
228 | } |
229 | } |
230 | |
231 | // No delegate or grab failed, start drag immediately |
232 | startDrag(image: m_delegateImage); |
233 | } |
234 | } |
235 | |
236 | void DeclarativeDragArea::mouseMoveEvent(QMouseEvent *event) |
237 | { |
238 | if (!m_enabled || QLineF(event->globalPosition(), m_buttonDownPos).length() < m_startDragDistance) { |
239 | return; |
240 | } |
241 | |
242 | // don't start drags on move for touch events, they'll be handled only by press and hold |
243 | // reset timer if moved more than m_startDragDistance |
244 | if (event->source() == Qt::MouseEventSynthesizedByQt) { |
245 | killTimer(id: m_pressAndHoldTimerId); |
246 | m_pressAndHoldTimerId = 0; |
247 | return; |
248 | } |
249 | |
250 | if (m_draggingJustStarted) { |
251 | // Grab delegate before starting drag |
252 | if (m_delegate) { |
253 | // Another grab is already in progress |
254 | if (m_grabResult) { |
255 | return; |
256 | } |
257 | m_grabResult = m_delegate->grabToImage(); |
258 | if (m_grabResult) { |
259 | connect(sender: m_grabResult.data(), signal: &QQuickItemGrabResult::ready, context: this, slot: [this]() { |
260 | startDrag(image: m_grabResult->image()); |
261 | m_grabResult.reset(); |
262 | }); |
263 | return; |
264 | } |
265 | } |
266 | |
267 | // No delegate or grab failed, start drag immediately |
268 | startDrag(image: m_delegateImage); |
269 | } |
270 | } |
271 | |
272 | bool DeclarativeDragArea::childMouseEventFilter(QQuickItem *item, QEvent *event) |
273 | { |
274 | if (!isEnabled()) { |
275 | return false; |
276 | } |
277 | |
278 | switch (event->type()) { |
279 | case QEvent::MouseButtonPress: { |
280 | QMouseEvent *me = static_cast<QMouseEvent *>(event); |
281 | // qDebug() << "press in dragarea"; |
282 | mousePressEvent(event: me); |
283 | break; |
284 | } |
285 | case QEvent::MouseMove: { |
286 | QMouseEvent *me = static_cast<QMouseEvent *>(event); |
287 | // qDebug() << "move in dragarea"; |
288 | mouseMoveEvent(event: me); |
289 | break; |
290 | } |
291 | case QEvent::MouseButtonRelease: { |
292 | QMouseEvent *me = static_cast<QMouseEvent *>(event); |
293 | // qDebug() << "release in dragarea"; |
294 | mouseReleaseEvent(event: me); |
295 | break; |
296 | } |
297 | default: |
298 | break; |
299 | } |
300 | |
301 | return QQuickItem::childMouseEventFilter(item, event); |
302 | } |
303 | |
304 | void DeclarativeDragArea::startDrag(const QImage &image) |
305 | { |
306 | grabMouse(); |
307 | m_draggingJustStarted = false; |
308 | |
309 | QDrag *drag = new QDrag(parent()); |
310 | DeclarativeMimeData *dataCopy = new DeclarativeMimeData(m_data); // Qt will take ownership of this copy and delete it. |
311 | drag->setMimeData(dataCopy); |
312 | |
313 | const qreal devicePixelRatio = window() ? window()->devicePixelRatio() : 1; |
314 | const int imageSize = 48 * devicePixelRatio; |
315 | |
316 | if (!image.isNull()) { |
317 | drag->setPixmap(QPixmap::fromImage(image)); |
318 | } else if (mimeData()->hasImage()) { |
319 | const QImage im = qvariant_cast<QImage>(v: mimeData()->imageData()); |
320 | drag->setPixmap(QPixmap::fromImage(image: im)); |
321 | } else if (mimeData()->hasColor()) { |
322 | QPixmap px(imageSize, imageSize); |
323 | px.fill(fillColor: mimeData()->color()); |
324 | drag->setPixmap(px); |
325 | } else { |
326 | // Icons otherwise |
327 | QStringList icons; |
328 | if (mimeData()->hasText()) { |
329 | icons << QStringLiteral("text-plain" ); |
330 | } |
331 | if (mimeData()->hasHtml()) { |
332 | icons << QStringLiteral("text-html" ); |
333 | } |
334 | if (mimeData()->hasUrls()) { |
335 | for (int i = 0; i < std::min<int>(a: 4, b: mimeData()->urls().size()); ++i) { |
336 | icons << QStringLiteral("text-html" ); |
337 | } |
338 | } |
339 | if (!icons.isEmpty()) { |
340 | QPixmap pm(imageSize * icons.count(), imageSize); |
341 | pm.fill(fillColor: Qt::transparent); |
342 | QPainter p(&pm); |
343 | int i = 0; |
344 | for (const QString &ic : std::as_const(t&: icons)) { |
345 | p.drawPixmap(p: QPoint(i * imageSize, 0), pm: QIcon::fromTheme(name: ic).pixmap(extent: imageSize)); |
346 | i++; |
347 | } |
348 | p.end(); |
349 | drag->setPixmap(pm); |
350 | } |
351 | } |
352 | |
353 | // drag->setHotSpot(QPoint(drag->pixmap().width()/2, drag->pixmap().height()/2)); // TODO: Make a property for that |
354 | // setCursor(Qt::OpenHandCursor); //TODO? Make a property for the cursor |
355 | |
356 | m_dragActive = true; |
357 | Q_EMIT dragActiveChanged(); |
358 | Q_EMIT dragStarted(); |
359 | |
360 | Qt::DropAction action = drag->exec(supportedActions: m_supportedActions, defaultAction: m_defaultAction); |
361 | setKeepMouseGrab(false); |
362 | |
363 | m_dragActive = false; |
364 | Q_EMIT dragActiveChanged(); |
365 | Q_EMIT drop(action); |
366 | |
367 | ungrabMouse(); |
368 | } |
369 | |
370 | #include "moc_DeclarativeDragArea.cpp" |
371 | |