1// Copyright (C) 2021 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 "qquickfiledialogdelegate_p.h"
5#include "qquickfiledialogdelegate_p_p.h"
6
7#include <QtCore/qfileinfo.h>
8#include <QtCore/qmimedata.h>
9#include <QtGui/qpa/qplatformtheme.h>
10#include <QtQml/QQmlFile>
11#include <QtQml/qqmlexpression.h>
12#include <QtQuick/private/qquicklistview_p.h>
13#include <QtQuick/private/qquickitemview_p_p.h>
14#include "qquicksidebar_p.h"
15#include "qquicksidebar_p_p.h"
16
17QT_BEGIN_NAMESPACE
18
19using namespace Qt::Literals::StringLiterals;
20
21void QQuickFileDialogDelegatePrivate::highlightFile()
22{
23 Q_Q(QQuickFileDialogDelegate);
24 QQuickListViewAttached *attached = static_cast<QQuickListViewAttached*>(
25 qmlAttachedPropertiesObject<QQuickListView>(obj: q));
26 if (!attached)
27 return;
28
29 QQmlContext *delegateContext = qmlContext(q);
30 if (!delegateContext)
31 return;
32
33 bool converted = false;
34 const int index = q->property(name: "index").toInt(ok: &converted);
35 if (converted) {
36 attached->view()->setCurrentIndex(index);
37 if (fileDialog)
38 fileDialog->setSelectedFile(file);
39 else if (folderDialog)
40 folderDialog->setSelectedFolder(file);
41 }
42}
43
44void QQuickFileDialogDelegatePrivate::chooseFile()
45{
46 const QFileInfo fileInfo(QQmlFile::urlToLocalFileOrQrc(file));
47 if (fileInfo.isDir()) {
48 // If it's a directory, navigate to it.
49 if (fileDialog)
50 fileDialog->setCurrentFolder(currentFolder: file);
51 else
52 folderDialog->setCurrentFolder(file);
53 } else {
54 Q_ASSERT(fileDialog);
55 // Otherwise it's a file, so select it and close the dialog.
56 fileDialog->setSelectedFile(file);
57
58 // Prioritize closing the dialog with QQuickDialogPrivate::handleClick() over QQuickDialog::accept()
59 const QQuickFileDialogImplAttached *attached = QQuickFileDialogImplPrivate::get(dialog: fileDialog)->attachedOrWarn();
60 if (Q_LIKELY(attached)) {
61 auto *openButton = attached->buttonBox()->standardButton(button: QPlatformDialogHelper::Open);
62 if (Q_LIKELY(openButton)) {
63 emit openButton->clicked();
64 return;
65 }
66 }
67 fileDialog->accept();
68 }
69}
70
71bool QQuickFileDialogDelegatePrivate::acceptKeyClick(Qt::Key key) const
72{
73 return key == Qt::Key_Return || key == Qt::Key_Enter;
74}
75
76QQuickFileDialogDelegate::QQuickFileDialogDelegate(QQuickItem *parent)
77 : QQuickItemDelegate(*(new QQuickFileDialogDelegatePrivate), parent)
78{
79 Q_D(QQuickFileDialogDelegate);
80 // Clicking and tabbing should result in it getting focus,
81 // as e.g. Ubuntu and Windows both allow tabbing through file dialogs.
82 setFocusPolicy(Qt::StrongFocus);
83 setCheckable(true);
84 QObjectPrivate::connect(sender: this, signal: &QQuickFileDialogDelegate::clicked,
85 receiverPrivate: d, slot: &QQuickFileDialogDelegatePrivate::highlightFile);
86 QObjectPrivate::connect(sender: this, signal: &QQuickFileDialogDelegate::doubleClicked,
87 receiverPrivate: d, slot: &QQuickFileDialogDelegatePrivate::chooseFile);
88}
89
90QQuickDialog *QQuickFileDialogDelegate::dialog() const
91{
92 Q_D(const QQuickFileDialogDelegate);
93 return d->dialog;
94}
95
96void QQuickFileDialogDelegate::setDialog(QQuickDialog *dialog)
97{
98 Q_D(QQuickFileDialogDelegate);
99 if (dialog == d->dialog)
100 return;
101
102 d->dialog = dialog;
103 d->fileDialog = qobject_cast<QQuickFileDialogImpl*>(object: dialog);
104 d->folderDialog = qobject_cast<QQuickFolderDialogImpl*>(object: dialog);
105 emit dialogChanged();
106
107 if (d->tapHandler)
108 d->destroyTapHandler();
109 if (d->fileDialog)
110 d->initTapHandler();
111
112
113}
114
115QUrl QQuickFileDialogDelegate::file() const
116{
117 Q_D(const QQuickFileDialogDelegate);
118 return d->file;
119}
120
121void QQuickFileDialogDelegate::setFile(const QUrl &file)
122{
123 Q_D(QQuickFileDialogDelegate);
124 QUrl adjustedFile = file;
125#ifdef Q_OS_WIN32
126 // Work around QTBUG-99105 (FolderListModel uses lowercase drive letter).
127 QString path = adjustedFile.path();
128 const int driveColonIndex = path.indexOf(QLatin1Char(':'));
129 if (driveColonIndex == 2) {
130 path.replace(1, 1, path.at(1).toUpper());
131 adjustedFile.setPath(path);
132 }
133#endif
134 if (adjustedFile == d->file)
135 return;
136
137 d->file = adjustedFile;
138 emit fileChanged();
139}
140
141void QQuickFileDialogDelegate::keyReleaseEvent(QKeyEvent *event)
142{
143 Q_D(QQuickFileDialogDelegate);
144 // We need to respond to being triggered by enter being pressed,
145 // but we can't use event->isAccepted() to check, because events are pre-accepted.
146 auto connection = QObjectPrivate::connect(sender: this, signal: &QQuickFileDialogDelegate::clicked,
147 receiverPrivate: d, slot: &QQuickFileDialogDelegatePrivate::chooseFile);
148
149 QQuickItemDelegate::keyReleaseEvent(event);
150
151 disconnect(connection);
152}
153
154void QQuickFileDialogDelegatePrivate::initTapHandler()
155{
156 Q_Q(QQuickFileDialogDelegate);
157 if (!tapHandler) {
158 tapHandler = new QQuickFileDialogTapHandler(q);
159
160 connect(sender: tapHandler, signal: &QQuickTapHandler::longPressed, receiverPrivate: this,
161 slot: &QQuickFileDialogDelegatePrivate::handleLongPress);
162 }
163}
164
165void QQuickFileDialogDelegatePrivate::destroyTapHandler()
166{
167 if (tapHandler)
168 delete tapHandler;
169}
170
171void QQuickFileDialogDelegatePrivate::handleLongPress()
172{
173 if (!tapHandler)
174 return;
175
176 tapHandler->m_state = QQuickFileDialogTapHandler::Tracking;
177 tapHandler->m_longPressed = true;
178}
179
180// ----------------------------------------------
181
182QQuickFileDialogTapHandler::QQuickFileDialogTapHandler(QQuickItem *parent)
183 : QQuickTapHandler(parent)
184{
185 // Set a grab permission that stops the flickable from stealing the drag.
186 setGrabPermissions(QQuickPointerHandler::CanTakeOverFromAnything);
187
188 // The drag threshold is used by the handler to know when to start a drag.
189 // We handle the drag inpendently, so set the threshold to a big number.
190 // This will guarantee that QQuickTapHandler::wantsEventPoint always returns
191 // true.
192 setDragThreshold(1000);
193}
194
195QQuickFileDialogImpl *QQuickFileDialogTapHandler::getFileDialogImpl() const
196{
197 auto *delegate = qobject_cast<QQuickFileDialogDelegate*>(object: parent());
198 auto *delegatePrivate = QQuickFileDialogDelegatePrivate::get(delegate);
199 return delegatePrivate->fileDialog;
200}
201
202void QQuickFileDialogTapHandler::grabFolder()
203{
204 if (m_drag.isNull())
205 return;
206
207 QQuickPalette *palette = [this]() -> QQuickPalette* {
208 auto *delegate = qobject_cast<QQuickFileDialogDelegate*>(object: parent());
209 if (delegate) {
210 QQuickDialog *dlg = delegate->dialog();
211 if (dlg) {
212 QQuickDialogPrivate *priv = QQuickDialogPrivate::get(dialog: dlg);
213 if (priv)
214 return priv->palette();
215 }
216 }
217 return nullptr;
218 }();
219
220 // TODO: use proper @Nx scaling
221 const auto src = ":/qt-project.org/imports/QtQuick/Dialogs/quickimpl/images/sidebar-folder.png"_L1;
222 QImage img = QImage(src).convertToFormat(f: QImage::Format_ARGB32);
223
224 if (!img.isNull() && palette) {
225 const QColor iconColor = palette->buttonText();
226 if (iconColor.alpha() > 0) {
227 // similar to what QQuickIconImage does
228 QPainter painter(&img);
229 painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
230 painter.fillRect(img.rect(), color: iconColor);
231 }
232 }
233
234 QPixmap pixmap = QPixmap::fromImage(image: std::move(img));
235 auto *mimeData = new QMimeData();
236 mimeData->setImageData(pixmap);
237 m_drag->setMimeData(mimeData);
238 m_drag->setPixmap(pixmap);
239}
240
241QUrl QQuickFileDialogTapHandler::getFolderUrlAtPress() const
242{
243 return qobject_cast<QQuickFileDialogDelegate*>(object: parent())->file().toLocalFile();
244}
245
246void QQuickFileDialogTapHandler::handleDrag(QQuickDragEvent *event)
247{
248 if (m_state == Dragging) {
249 auto *fileDialogImpl = getFileDialogImpl();
250 auto *attached = QQuickFileDialogImplPrivate::get(dialog: fileDialogImpl)->attachedOrWarn();
251 if (Q_LIKELY(attached)) {
252 auto *sideBar = attached->sideBar();
253 if (Q_LIKELY(sideBar)) {
254 auto *sideBarPrivate = QQuickSideBarPrivate::get(sidebar: sideBar);
255 const auto pos = QPoint(event->x(), event->y());
256 sideBarPrivate->setShowAddFavoriteDelegate(sideBar->contains(point: pos));
257 if (sideBarPrivate->showAddFavoriteDelegate()) {
258 const QList<QQuickItem *> items = sideBar->contentChildren().toList<QList<QQuickItem *>>();
259 // iterate through the children until we have counted all the folder paths. The next button will
260 // be the addFavoriteDelegate
261 const int addFavoritePos = sideBar->effectiveFolderPaths().size();
262 int currentPos = 0;
263 for (int i = 0; i < items.length(); i++) {
264 if (auto *button = qobject_cast<QQuickAbstractButton *>(object: items.at(i))) {
265 if (currentPos == addFavoritePos) {
266 // check if the pointer position is within the add favorite button
267 const QRectF bBox = button->mapRectToItem(item: sideBar, rect: button->boundingRect());
268 sideBarPrivate->setAddFavoriteDelegateHovered(bBox.contains(p: pos));
269 break;
270 } else {
271 currentPos++;
272 }
273 }
274 }
275 }
276 }
277 }
278 }
279}
280
281void QQuickFileDialogTapHandler::handleDrop(QQuickDragEvent *event)
282{
283 Q_UNUSED(event);
284 if (m_state == Dragging) {
285 if (!m_sourceUrl.isEmpty()) {
286 auto *fileDialogImpl = getFileDialogImpl();
287 auto *attached = QQuickFileDialogImplPrivate::get(dialog: fileDialogImpl)->attachedOrWarn();
288 if (Q_LIKELY(attached)) {
289 auto *sideBar = attached->sideBar();
290 if (Q_LIKELY(sideBar)) {
291 auto *sideBarPrivate = QQuickSideBarPrivate::get(sidebar: sideBar);
292 // this cannot be handled in handleDrag because handleDrag is connected to the drop
293 // area, and so won't run when the cursor is outside of it
294 if (sideBarPrivate->addFavoriteDelegateHovered())
295 sideBarPrivate->addFavorite(favorite: m_sourceUrl);
296 sideBarPrivate->setShowAddFavoriteDelegate(false);
297 }
298 }
299 }
300 m_state = DraggingFinished;
301 }
302}
303
304void QQuickFileDialogTapHandler::handleContainsDragChanged()
305{
306 if (m_state == Dragging) {
307 auto *fileDialogImpl = getFileDialogImpl();
308 auto *attached = QQuickFileDialogImplPrivate::get(dialog: fileDialogImpl)->attachedOrWarn();
309 if (Q_LIKELY(attached)) {
310 auto *sideBar = attached->sideBar();
311 if (Q_LIKELY(sideBar && m_dropArea)) {
312 auto *sideBarPrivate = QQuickSideBarPrivate::get(sidebar: sideBar);
313 if (m_dropArea->containsDrag()) {
314 sideBarPrivate->setShowAddFavoriteDelegate(true);
315 } else {
316 sideBarPrivate->setShowAddFavoriteDelegate(false);
317 sideBarPrivate->setAddFavoriteDelegateHovered(false);
318 }
319 }
320 }
321 }
322}
323
324void QQuickFileDialogTapHandler::resetDragData()
325{
326 if (m_state != Listening) {
327 m_state = Listening;
328 m_sourceUrl = QUrl();
329 if (!m_drag.isNull()) {
330 m_drag->disconnect();
331 delete m_drag;
332 }
333 if (!m_dropArea.isNull()) {
334 m_dropArea->disconnect();
335 delete m_dropArea;
336 }
337
338 m_longPressed = false;
339 }
340}
341
342void QQuickFileDialogTapHandler::handleEventPoint(QPointerEvent *event, QEventPoint &point)
343{
344 QQuickTapHandler::handleEventPoint(event, point);
345
346 auto *fileDialogDelegate = qobject_cast<QQuickFileDialogDelegate *>(object: parent());
347 auto *fileDialogImpl = getFileDialogImpl();
348 if (Q_UNLIKELY(!fileDialogImpl))
349 return;
350 auto *fileDialogImplPrivate = QQuickFileDialogImplPrivate::get(dialog: fileDialogImpl);
351 QQuickFileDialogImplAttached *attached = fileDialogImplPrivate->attachedOrWarn();
352 if (Q_UNLIKELY(!attached || !attached->sideBar()))
353 return;
354
355 if (m_state == DraggingFinished)
356 resetDragData();
357
358 if (point.state() == QEventPoint::Pressed) {
359 resetDragData();
360 // Activate the passive grab to get further move updates
361 setPassiveGrab(event, point, grab: true);
362 } else if (point.state() == QEventPoint::Released) {
363 resetDragData();
364 } else if (point.state() == QEventPoint::Updated && m_longPressed) {
365 // Check to see that the movement can be considered as dragging
366 const qreal distX = point.position().x() - point.pressPosition().x();
367 const qreal distY = point.position().y() - point.pressPosition().y();
368 // consider the squared distance to be optimal
369 const qreal dragDistSq = distX * distX + distY * distY;
370 if (dragDistSq > qPow(qApp->styleHints()->startDragDistance(), y: 2)) {
371 switch (m_state) {
372 case Tracking: {
373 // set the drag
374 if (m_drag.isNull())
375 m_drag = new QDrag(fileDialogDelegate);
376 // Set the drop area
377 if (m_dropArea.isNull()) {
378 auto *sideBar = attached->sideBar();
379 m_dropArea = new QQuickDropArea(sideBar);
380 m_dropArea->setSize(sideBar->size());
381 connect(sender: m_dropArea, signal: &QQuickDropArea::positionChanged, context: this,
382 slot: &QQuickFileDialogTapHandler::handleDrag);
383 connect(sender: m_dropArea, signal: &QQuickDropArea::dropped, context: this,
384 slot: &QQuickFileDialogTapHandler::handleDrop);
385 connect(sender: m_dropArea, signal: &QQuickDropArea::containsDragChanged, context: this,
386 slot: &QQuickFileDialogTapHandler::handleContainsDragChanged);
387 }
388
389 m_sourceUrl = fileDialogDelegate->file();
390 // set up the grab
391 grabFolder();
392 m_state = DraggingStarted;
393 } break;
394
395 case DraggingStarted: {
396 if (m_drag && m_drag->mimeData()) {
397 if (auto *item = qobject_cast<QQuickItem *>(o: m_drag->source())) {
398 Q_UNUSED(item);
399 m_state = Dragging;
400 // start the drag
401 m_drag->exec();
402 // If the state still remains dragging, then the drop happened outside
403 // the drop area
404 if (m_state == Dragging)
405 resetDragData();
406 }
407 }
408 } break;
409
410 default:
411 break;
412 }
413 }
414 }
415}
416
417bool QQuickFileDialogTapHandler::wantsEventPoint(const QPointerEvent *event, const QEventPoint &point)
418{
419 if (!QQuickTapHandler::wantsEventPoint(event, point) || event->type() == QEvent::Type::Wheel)
420 return false;
421
422 // we only want the event point if the delegate contains a directory and not a file
423 auto *fileDialogDelegate = qobject_cast<QQuickFileDialogDelegate *>(object: parent());
424 QFileInfo info(fileDialogDelegate->file().toLocalFile());
425 if (info.isDir())
426 return true;
427
428 return false;
429}
430
431QT_END_NAMESPACE
432
433#include "moc_qquickfiledialogdelegate_p.cpp"
434

source code of qtdeclarative/src/quickdialogs/quickdialogsquickimpl/qquickfiledialogdelegate.cpp