Revision control
Copy as Markdown
// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
// Qt-Security score:significant reason:default
#include "qquickfolderdialogimpl_p.h"
#include "qquickfolderdialogimpl_p_p.h"
#include <QtCore/qloggingcategory.h>
#include <QtQuickTemplates2/private/qquickdialogbuttonbox_p_p.h>
#include "qquickfiledialogdelegate_p.h"
#include "qquickfolderbreadcrumbbar_p.h"
QT_BEGIN_NAMESPACE
Q_STATIC_LOGGING_CATEGORY(lcFolderDialogCurrentFolder, "qt.quick.dialogs.quickfolderdialogimpl.currentFolder")
Q_STATIC_LOGGING_CATEGORY(lcFolderDialogSelectedFolder, "qt.quick.dialogs.quickfolderdialogimpl.selectedFolder")
Q_STATIC_LOGGING_CATEGORY(lcFolderDialogOptions, "qt.quick.dialogs.quickfolderdialogimpl.options")
QQuickFolderDialogImplPrivate::QQuickFolderDialogImplPrivate()
{
}
void QQuickFolderDialogImplPrivate::updateEnabled()
{
Q_Q(QQuickFolderDialogImpl);
if (!buttonBox)
return;
QQuickFolderDialogImplAttached *attached = attachedOrWarn();
if (!attached)
return;
auto openButton = buttonBox->standardButton(QPlatformDialogHelper::Open);
if (!openButton) {
qmlWarning(q).nospace() << "Can't update Open button's enabled state because it wasn't found";
return;
}
openButton->setEnabled(!selectedFolder.isEmpty() && attached->breadcrumbBar()
&& !attached->breadcrumbBar()->textField()->isVisible());
}
/*!
\internal
Ensures that a folder is always selected after a change in \c currentFolder.
\a oldFolderPath is the previous value of \c currentFolder.
*/
void QQuickFolderDialogImplPrivate::updateSelectedFolder(const QString &oldFolderPath)
{
Q_Q(QQuickFolderDialogImpl);
QQuickFolderDialogImplAttached *attached = attachedOrWarn();
if (!attached || !attached->folderDialogListView())
return;
QString newSelectedFolderPath;
int newSelectedFolderIndex = 0;
const QString newFolderPath = QQmlFile::urlToLocalFileOrQrc(currentFolder);
if (!oldFolderPath.isEmpty() && !newFolderPath.isEmpty()) {
// If the user went up a directory (or several), we should set
// selectedFolder to be the directory that we were in (or
// its closest ancestor that is a child of the new directory).
// E.g. if oldFolderPath is /foo/bar/baz/abc/xyz, and newFolderPath is /foo/bar,
// then we want to set selectedFolder to be /foo/bar/baz.
const int indexOfFolder = oldFolderPath.indexOf(newFolderPath);
if (indexOfFolder != -1) {
// [folder]
// [ oldFolderPath ]
// /foo/bar/baz/abc/xyz
// [rel...Paths]
QStringList relativePaths = oldFolderPath.mid(indexOfFolder + newFolderPath.size()).split(QLatin1Char('/'), Qt::SkipEmptyParts);
newSelectedFolderPath = newFolderPath + QLatin1Char('/') + relativePaths.first();
// Now find the index of that directory so that we can set the ListView's currentIndex to it.
const QDir newFolderDir(newFolderPath);
// Just to be safe...
if (!newFolderDir.exists()) {
qmlWarning(q) << "Directory" << newSelectedFolderPath << "doesn't exist; can't get a file entry list for it";
return;
}
const QFileInfoList dirs = newFolderDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::DirsFirst);
const QFileInfo newSelectedFileInfo(newSelectedFolderPath);
// The directory can contain files, but since we put dirs first, that should never affect the indices.
newSelectedFolderIndex = dirs.indexOf(newSelectedFileInfo);
}
}
if (newSelectedFolderPath.isEmpty()) {
// When entering into a directory that isn't a parent of the old one, the first
// file delegate should be selected.
// TODO: is there a cheaper way to do this? QDirIterator doesn't support sorting,
// so we can't use that. QQuickFolderListModel uses threads to fetch its data,
// so should be considered asynchronous. We might be able to use it, but it would
// complicate the code even more...
QDir newFolderDir(newFolderPath);
if (newFolderDir.exists()) {
const QFileInfoList files = newFolderDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::DirsFirst);
if (!files.isEmpty())
newSelectedFolderPath = files.first().absoluteFilePath();
}
}
const bool folderSelected = !newSelectedFolderPath.isEmpty();
q->setSelectedFolder(folderSelected ? QUrl::fromLocalFile(newSelectedFolderPath) : QUrl());
{
// Set the appropriate currentIndex for the selected folder. We block signals from ListView
// because we don't want folderDialogListViewCurrentIndexChanged to be called, as the file
// it gets from the delegate will not be up-to-date (but most importantly because we already
// just set the selected folder).
QSignalBlocker blocker(attached->folderDialogListView());
attached->folderDialogListView()->setCurrentIndex(folderSelected ? newSelectedFolderIndex : -1);
}
if (folderSelected) {
if (QQuickItem *currentItem = attached->folderDialogListView()->currentItem())
currentItem->forceActiveFocus();
}
}
void QQuickFolderDialogImplPrivate::handleAccept()
{
// Let handleClick take care of calling accept().
}
void QQuickFolderDialogImplPrivate::handleClick(QQuickAbstractButton *button)
{
Q_Q(QQuickFolderDialogImpl);
if (buttonRole(button) == QPlatformDialogHelper::AcceptRole && selectedFolder.isValid()) {
q->setSelectedFolder(selectedFolder);
q->accept();
}
}
/*!
\class QQuickFolderDialogImpl
\internal
An interface that QQuickFolderDialog can use to access the non-native Qt Quick FolderDialog.
Both this and the native implementations are created in QQuickAbstractDialog::create().
*/
QQuickFolderDialogImpl::QQuickFolderDialogImpl(QObject *parent)
: QQuickDialog(*(new QQuickFolderDialogImplPrivate), parent)
{
}
QQuickFolderDialogImplAttached *QQuickFolderDialogImpl::qmlAttachedProperties(QObject *object)
{
return new QQuickFolderDialogImplAttached(object);
}
QUrl QQuickFolderDialogImpl::currentFolder() const
{
Q_D(const QQuickFolderDialogImpl);
return d->currentFolder;
}
void QQuickFolderDialogImpl::setCurrentFolder(const QUrl ¤tFolder)
{
qCDebug(lcFolderDialogCurrentFolder) << "setCurrentFolder called with" << currentFolder;
Q_D(QQuickFolderDialogImpl);
if (currentFolder == d->currentFolder)
return;
const QString oldFolderPath = QQmlFile::urlToLocalFileOrQrc(d->currentFolder);
d->currentFolder = currentFolder;
d->updateSelectedFolder(oldFolderPath);
emit currentFolderChanged(d->currentFolder);
}
QUrl QQuickFolderDialogImpl::selectedFolder() const
{
Q_D(const QQuickFolderDialogImpl);
return d->selectedFolder;
}
void QQuickFolderDialogImpl::setSelectedFolder(const QUrl &selectedFolder)
{
Q_D(QQuickFolderDialogImpl);
qCDebug(lcFolderDialogSelectedFolder).nospace() << "setSelectedFolder called with selectedFolder "
<< selectedFolder << " (d->selectedFolder is " << d->selectedFolder << ")";
if (selectedFolder == d->selectedFolder)
return;
d->selectedFolder = selectedFolder;
d->updateEnabled();
emit selectedFolderChanged(selectedFolder);
}
QSharedPointer<QFileDialogOptions> QQuickFolderDialogImpl::options() const
{
Q_D(const QQuickFolderDialogImpl);
return d->options;
}
void QQuickFolderDialogImpl::setOptions(const QSharedPointer<QFileDialogOptions> &options)
{
qCDebug(lcFolderDialogOptions).nospace() << "setOptions called with:"
<< " acceptMode=" << options->acceptMode()
<< " fileMode=" << options->fileMode()
<< " initialDirectory=" << options->initialDirectory();
Q_D(QQuickFolderDialogImpl);
d->options = options;
}
/*!
\internal
These allow QQuickPlatformFileDialog::show() to set custom labels on the
dialog buttons without having to know about/go through QQuickFolderDialogImplAttached
and QQuickDialogButtonBox.
*/
void QQuickFolderDialogImpl::setAcceptLabel(const QString &label)
{
Q_D(QQuickFolderDialogImpl);
d->acceptLabel = label;
QQuickFolderDialogImplAttached *attached = d->attachedOrWarn();
if (!attached)
return;
auto acceptButton = d->buttonBox->standardButton(QPlatformDialogHelper::Open);
if (!acceptButton) {
qmlWarning(this).nospace() << "Can't set accept label to " << label
<< "; failed to find Open button in DialogButtonBox of " << this;
return;
}
acceptButton->setText(!label.isEmpty()
? label : QQuickDialogButtonBoxPrivate::buttonText(QPlatformDialogHelper::Open));
}
void QQuickFolderDialogImpl::setRejectLabel(const QString &label)
{
Q_D(QQuickFolderDialogImpl);
d->rejectLabel = label;
if (!d->buttonBox)
return;
auto rejectButton = d->buttonBox->standardButton(QPlatformDialogHelper::Cancel);
if (!rejectButton) {
qmlWarning(this).nospace() << "Can't set reject label to " << label
<< "; failed to find Open button in DialogButtonBox of " << this;
return;
}
rejectButton->setText(!label.isEmpty()
? label : QQuickDialogButtonBoxPrivate::buttonText(QPlatformDialogHelper::Cancel));
}
void QQuickFolderDialogImpl::componentComplete()
{
Q_D(QQuickFolderDialogImpl);
QQuickDialog::componentComplete();
// Find the right-most button and set its key navigation so that
// tab moves focus to the breadcrumb bar's up button. I tried
// doing this via KeyNavigation on the DialogButtonBox in QML,
// but it didn't work (probably because it's not the right item).
QQuickFolderDialogImplAttached *attached = d->attachedOrWarn();
if (!attached)
return;
Q_ASSERT(d->buttonBox);
const int buttonCount = d->buttonBox->count();
if (buttonCount == 0)
return;
QQuickAbstractButton *rightMostButton = qobject_cast<QQuickAbstractButton *>(
d->buttonBox->itemAt(buttonCount - 1));
if (!rightMostButton) {
qmlWarning(this) << "Can't find right-most button in DialogButtonBox";
return;
}
auto keyNavigationAttached = QQuickKeyNavigationAttached::qmlAttachedProperties(rightMostButton);
if (!keyNavigationAttached) {
qmlWarning(this) << "Can't create attached KeyNavigation object on" << QDebug::toString(rightMostButton);
return;
}
keyNavigationAttached->setTab(attached->breadcrumbBar()->upButton());
}
void QQuickFolderDialogImpl::itemChange(QQuickItem::ItemChange change, const QQuickItem::ItemChangeData &data)
{
Q_D(QQuickFolderDialogImpl);
QQuickDialog::itemChange(change, data);
if (change != QQuickItem::ItemVisibleHasChanged || !isComponentComplete() || !data.boolValue)
return;
QQuickFolderDialogImplAttached *attached = d->attachedOrWarn();
if (!attached)
return;
attached->folderDialogListView()->forceActiveFocus();
d->updateEnabled();
}
QQuickFolderDialogImplAttached *QQuickFolderDialogImplPrivate::attachedOrWarn()
{
Q_Q(QQuickFolderDialogImpl);
QQuickFolderDialogImplAttached *attached = static_cast<QQuickFolderDialogImplAttached*>(
qmlAttachedPropertiesObject<QQuickFolderDialogImpl>(q));
if (!attached)
qmlWarning(q) << "Expected FileDialogImpl attached object to be present on" << this;
return attached;
}
void QQuickFolderDialogImplAttachedPrivate::folderDialogListViewCurrentIndexChanged()
{
auto folderDialogImpl = qobject_cast<QQuickFolderDialogImpl*>(parent);
if (!folderDialogImpl)
return;
auto folderDialogDelegate = qobject_cast<QQuickFileDialogDelegate*>(folderDialogListView->currentItem());
if (!folderDialogDelegate)
return;
folderDialogImpl->setSelectedFolder(folderDialogDelegate->file());
}
QQuickFolderDialogImplAttached::QQuickFolderDialogImplAttached(QObject *parent)
: QObject(*(new QQuickFolderDialogImplAttachedPrivate), parent)
{
if (!qobject_cast<QQuickFolderDialogImpl*>(parent)) {
qmlWarning(this) << "FolderDialogImpl attached properties should only be "
<< "accessed through the root FileDialogImpl instance";
}
}
QQuickListView *QQuickFolderDialogImplAttached::folderDialogListView() const
{
Q_D(const QQuickFolderDialogImplAttached);
return d->folderDialogListView;
}
void QQuickFolderDialogImplAttached::setFolderDialogListView(QQuickListView *folderDialogListView)
{
Q_D(QQuickFolderDialogImplAttached);
if (folderDialogListView == d->folderDialogListView)
return;
d->folderDialogListView = folderDialogListView;
QObjectPrivate::connect(d->folderDialogListView, &QQuickListView::currentIndexChanged,
d, &QQuickFolderDialogImplAttachedPrivate::folderDialogListViewCurrentIndexChanged);
emit folderDialogListViewChanged();
}
QQuickFolderBreadcrumbBar *QQuickFolderDialogImplAttached::breadcrumbBar() const
{
Q_D(const QQuickFolderDialogImplAttached);
return d->breadcrumbBar;
}
void QQuickFolderDialogImplAttached::setBreadcrumbBar(QQuickFolderBreadcrumbBar *breadcrumbBar)
{
Q_D(QQuickFolderDialogImplAttached);
if (breadcrumbBar == d->breadcrumbBar)
return;
d->breadcrumbBar = breadcrumbBar;
emit breadcrumbBarChanged();
}
QT_END_NAMESPACE
#include "moc_qquickfolderdialogimpl_p.cpp"