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
26DeclarativeDragArea::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
47DeclarativeDragArea::~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*/
58QQuickItem *DeclarativeDragArea::delegate() const
59{
60 return m_delegate;
61}
62
63void DeclarativeDragArea::setDelegate(QQuickItem *delegate)
64{
65 if (m_delegate != delegate) {
66 // qDebug() << " ______________________________________________ " << delegate;
67 m_delegate = delegate;
68 Q_EMIT delegateChanged();
69 }
70}
71void 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*/
80QQuickItem *DeclarativeDragArea::source() const
81{
82 return m_source;
83}
84
85void DeclarativeDragArea::setSource(QQuickItem *source)
86{
87 if (m_source != source) {
88 m_source = source;
89 Q_EMIT sourceChanged();
90 }
91}
92
93void DeclarativeDragArea::resetSource()
94{
95 setSource(nullptr);
96}
97
98bool DeclarativeDragArea::dragActive() const
99{
100 return m_dragActive;
101}
102
103// target
104QQuickItem *DeclarativeDragArea::target() const
105{
106 // TODO: implement me
107 return nullptr;
108}
109
110// data
111DeclarativeMimeData *DeclarativeDragArea::mimeData() const
112{
113 return m_data;
114}
115
116// startDragDistance
117int DeclarativeDragArea::startDragDistance() const
118{
119 return m_startDragDistance;
120}
121
122void 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
133QVariant DeclarativeDragArea::delegateImage() const
134{
135 return m_delegateImage;
136}
137
138void 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
156bool DeclarativeDragArea::isEnabled() const
157{
158 return m_enabled;
159}
160void DeclarativeDragArea::setEnabled(bool enabled)
161{
162 if (enabled != m_enabled) {
163 m_enabled = enabled;
164 Q_EMIT enabledChanged();
165 }
166}
167
168// supported actions
169Qt::DropActions DeclarativeDragArea::supportedActions() const
170{
171 return m_supportedActions;
172}
173void 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
182Qt::DropAction DeclarativeDragArea::defaultAction() const
183{
184 return m_defaultAction;
185}
186void DeclarativeDragArea::setDefaultAction(Qt::DropAction action)
187{
188 if (action != m_defaultAction) {
189 m_defaultAction = action;
190 Q_EMIT defaultActionChanged();
191 }
192}
193
194void 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
202void 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
212void 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
236void 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
272bool 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
304void 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

source code of kdeclarative/src/qmlcontrols/draganddrop/DeclarativeDragArea.cpp