File indexing completed on 2024-11-17 05:01:37

0001 /*  This file is part of the KDE libraries
0002     SPDX-FileCopyrightText: 2013 Aleix Pol Gonzalez <aleixpol@blue-systems.com>
0003     SPDX-FileCopyrightText: 2014 Martin Klapetek <mklapetek@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
0006 */
0007 
0008 #include "kdeplatformfiledialoghelper.h"
0009 #include "kdirselectdialog_p.h"
0010 
0011 #include <KIO/StatJob>
0012 #include <KJobWidgets>
0013 #include <KProtocolInfo>
0014 #include <KSharedConfig>
0015 #include <KWindowConfig>
0016 #include <kdiroperator.h>
0017 #include <kfilefiltercombo.h>
0018 #include <kfilewidget.h>
0019 #include <kio_version.h>
0020 #include <klocalizedstring.h>
0021 
0022 #include <QDialogButtonBox>
0023 #include <QMimeDatabase>
0024 #include <QPushButton>
0025 #include <QTextStream>
0026 #include <QVBoxLayout>
0027 #include <QWindow>
0028 
0029 /*
0030  * Map a Qt filter string into a KDE one.
0031  */
0032 QString KDEPlatformFileDialogHelper::qt2KdeFilter(const QStringList &f)
0033 {
0034     QStringList qtFilters(f);
0035     // escape slashes to avoid them being interpreted as MIME types
0036     qtFilters.replaceInStrings(QStringLiteral("/"), QStringLiteral("\\/"));
0037 
0038     QStringList outFilters;
0039 
0040     for (const QString &filter : std::as_const(qtFilters)) {
0041         const QString name = filter.left(filter.indexOf(QLatin1Char('('))).trimmed();
0042         const QStringList extensions = cleanFilterList(filter);
0043 
0044         if (name.isEmpty()) {
0045             outFilters << extensions.join(QLatin1Char(' '));
0046         } else {
0047             outFilters << extensions.join(QLatin1Char(' ')) + QLatin1Char('|') + name;
0048         }
0049     }
0050 
0051     return outFilters.join(QLatin1Char('\n'));
0052 }
0053 
0054 namespace
0055 {
0056 /*
0057  * Map a KDE filter string into a Qt one.
0058  */
0059 static QString kde2QtFilter(const QStringList &list, const QString &kde, const QString &filterText)
0060 {
0061     QStringList::const_iterator it(list.constBegin()), end(list.constEnd());
0062     int pos;
0063 
0064     for (; it != end; ++it) {
0065         if (-1 != (pos = it->indexOf(kde)) && pos > 0 && (QLatin1Char('(') == (*it)[pos - 1] || QLatin1Char(' ') == (*it)[pos - 1])
0066             && it->length() >= kde.length() + pos && (QLatin1Char(')') == (*it)[pos + kde.length()] || QLatin1Char(' ') == (*it)[pos + kde.length()])
0067             && (filterText.isEmpty() || it->startsWith(filterText))) {
0068             return *it;
0069         }
0070     }
0071     return QString();
0072 }
0073 }
0074 
0075 KDEPlatformFileDialog::KDEPlatformFileDialog()
0076     : KDEPlatformFileDialogBase()
0077     , m_fileWidget(new KFileWidget(QUrl(), this))
0078 {
0079     auto v = new QVBoxLayout;
0080     v->setContentsMargins({});
0081     setLayout(v);
0082     connect(m_fileWidget, &KFileWidget::filterChanged, this, &KDEPlatformFileDialogBase::filterSelected);
0083     layout()->addWidget(m_fileWidget);
0084     m_fileWidget->okButton()->show();
0085     m_fileWidget->cancelButton()->show();
0086 
0087     connect(this, &QDialog::rejected, m_fileWidget, &KFileWidget::slotCancel);
0088     // Also call the cancel function when the dialog is closed via the escape key
0089     // or titlebar close button to make sure we always save the view config
0090     connect(this, &KDEPlatformFileDialog::rejected, m_fileWidget, &KFileWidget::slotCancel);
0091     connect(m_fileWidget->okButton(), &QAbstractButton::clicked, m_fileWidget, &KFileWidget::slotOk);
0092     connect(m_fileWidget, &KFileWidget::accepted, m_fileWidget, &KFileWidget::accept);
0093     connect(m_fileWidget, &KFileWidget::accepted, this, &QDialog::accept);
0094     connect(m_fileWidget->cancelButton(), &QAbstractButton::clicked, this, &QDialog::reject);
0095     connect(m_fileWidget->dirOperator(), &KDirOperator::urlEntered, this, &KDEPlatformFileDialogBase::directoryEntered);
0096 }
0097 
0098 QUrl KDEPlatformFileDialog::directory()
0099 {
0100     return m_fileWidget->baseUrl();
0101 }
0102 
0103 QList<QUrl> KDEPlatformFileDialog::selectedFiles()
0104 {
0105     return m_fileWidget->selectedUrls();
0106 }
0107 
0108 void KDEPlatformFileDialog::selectFile(const QUrl &filename)
0109 {
0110     const QUrl dirUrl = filename.adjusted(QUrl::RemoveFilename);
0111     m_fileWidget->setUrl(dirUrl);
0112     m_fileWidget->setSelectedUrl(filename);
0113 }
0114 
0115 void KDEPlatformFileDialog::setViewMode(QFileDialogOptions::ViewMode view)
0116 {
0117     switch (view) {
0118     case QFileDialogOptions::ViewMode::Detail:
0119         m_fileWidget->setViewMode(KFile::FileView::Detail);
0120         break;
0121     case QFileDialogOptions::ViewMode::List:
0122         m_fileWidget->setViewMode(KFile::FileView::Simple);
0123         break;
0124     default:
0125         m_fileWidget->setViewMode(KFile::FileView::Default);
0126         break;
0127     }
0128 }
0129 
0130 void KDEPlatformFileDialog::setFileMode(QFileDialogOptions::FileMode mode)
0131 {
0132     switch (mode) {
0133     case QFileDialogOptions::FileMode::AnyFile:
0134         m_fileWidget->setMode(KFile::File);
0135         break;
0136     case QFileDialogOptions::FileMode::ExistingFile:
0137         m_fileWidget->setMode(KFile::Mode::File | KFile::Mode::ExistingOnly);
0138         break;
0139     case QFileDialogOptions::FileMode::Directory:
0140         m_fileWidget->setMode(KFile::Mode::Directory | KFile::Mode::ExistingOnly);
0141         break;
0142     case QFileDialogOptions::FileMode::ExistingFiles:
0143         m_fileWidget->setMode(KFile::Mode::Files | KFile::Mode::ExistingOnly);
0144         break;
0145     default:
0146         m_fileWidget->setMode(KFile::File);
0147         break;
0148     }
0149 }
0150 
0151 void KDEPlatformFileDialog::setCustomLabel(QFileDialogOptions::DialogLabel label, const QString &text)
0152 {
0153     if (label == QFileDialogOptions::Accept) { // OK button
0154         m_fileWidget->okButton()->setText(text);
0155     } else if (label == QFileDialogOptions::Reject) { // Cancel button
0156         m_fileWidget->cancelButton()->setText(text);
0157     } else if (label == QFileDialogOptions::LookIn) { // Location label
0158         m_fileWidget->setLocationLabel(text);
0159     }
0160 }
0161 
0162 QString KDEPlatformFileDialog::selectedMimeTypeFilter()
0163 {
0164     if (m_fileWidget->filterWidget()->isMimeFilter()) {
0165         const auto mimeTypeFromFilter = QMimeDatabase().mimeTypeForName(m_fileWidget->filterWidget()->currentFilter());
0166         // If one does not call selectMimeTypeFilter(), KFileFilterCombo::currentFilter() returns invalid mimeTypes,
0167         // such as "application/json application/zip".
0168         if (mimeTypeFromFilter.isValid()) {
0169             return mimeTypeFromFilter.name();
0170         }
0171     }
0172 
0173     if (selectedFiles().isEmpty()) {
0174         return QString();
0175     }
0176 
0177     // Works for both KFile::File and KFile::Files modes.
0178     return QMimeDatabase().mimeTypeForUrl(selectedFiles().at(0)).name();
0179 }
0180 
0181 QString KDEPlatformFileDialog::selectedNameFilter()
0182 {
0183     return m_fileWidget->filterWidget()->currentFilter();
0184 }
0185 
0186 QString KDEPlatformFileDialog::currentFilterText()
0187 {
0188     return m_fileWidget->filterWidget()->currentText();
0189 }
0190 
0191 void KDEPlatformFileDialog::selectMimeTypeFilter(const QString &filter)
0192 {
0193     m_fileWidget->filterWidget()->setCurrentFilter(filter);
0194 }
0195 
0196 void KDEPlatformFileDialog::selectNameFilter(const QString &filter)
0197 {
0198     m_fileWidget->filterWidget()->setCurrentFilter(filter);
0199 }
0200 
0201 void KDEPlatformFileDialog::setDirectory(const QUrl &directory)
0202 {
0203     if (!directory.isLocalFile()) {
0204         // Short-circuit: Avoid stat if the effective URL hasn't changed
0205         if (directory == m_fileWidget->baseUrl()) {
0206             return;
0207         }
0208 
0209         // Qt can not determine if the remote URL points to a file or a
0210         // directory, that is why options()->initialDirectory() always returns
0211         // the full URL.
0212         KIO::StatJob *job = KIO::stat(directory);
0213         KJobWidgets::setWindow(job, this);
0214         if (job->exec()) {
0215             KIO::UDSEntry entry = job->statResult();
0216             if (!entry.isDir()) {
0217                 // this is probably a file remove the file part
0218                 m_fileWidget->setUrl(directory.adjusted(QUrl::RemoveFilename));
0219                 m_fileWidget->setSelectedUrl(directory);
0220             } else {
0221                 m_fileWidget->setUrl(directory);
0222             }
0223         }
0224     } else {
0225         m_fileWidget->setUrl(directory);
0226     }
0227 }
0228 
0229 bool KDEPlatformFileDialogHelper::isSupportedUrl(const QUrl &url) const
0230 {
0231     return KProtocolInfo::protocols().contains(url.scheme());
0232 }
0233 
0234 ////////////////////////////////////////////////
0235 
0236 KDEPlatformFileDialogHelper::KDEPlatformFileDialogHelper()
0237     : QPlatformFileDialogHelper()
0238     , m_dialog(new KDEPlatformFileDialog)
0239 {
0240     connect(m_dialog, &KDEPlatformFileDialogBase::closed, this, &KDEPlatformFileDialogHelper::saveSize);
0241     connect(m_dialog, &QDialog::finished, this, &KDEPlatformFileDialogHelper::saveSize);
0242     connect(m_dialog, &KDEPlatformFileDialogBase::currentChanged, this, &QPlatformFileDialogHelper::currentChanged);
0243     connect(m_dialog, &KDEPlatformFileDialogBase::directoryEntered, this, &QPlatformFileDialogHelper::directoryEntered);
0244     connect(m_dialog, &KDEPlatformFileDialogBase::fileSelected, this, &QPlatformFileDialogHelper::fileSelected);
0245     connect(m_dialog, &KDEPlatformFileDialogBase::filesSelected, this, &QPlatformFileDialogHelper::filesSelected);
0246     connect(m_dialog, &KDEPlatformFileDialogBase::filterSelected, this, &QPlatformFileDialogHelper::filterSelected);
0247     connect(m_dialog, &QDialog::accepted, this, &QPlatformDialogHelper::accept);
0248     connect(m_dialog, &QDialog::rejected, this, &QPlatformDialogHelper::reject);
0249 }
0250 
0251 KDEPlatformFileDialogHelper::~KDEPlatformFileDialogHelper()
0252 {
0253     saveSize();
0254     delete m_dialog;
0255 }
0256 
0257 void KDEPlatformFileDialogHelper::initializeDialog()
0258 {
0259     m_dialogInitialized = true;
0260     if (options()->testOption(QFileDialogOptions::ShowDirsOnly)) {
0261         m_dialog->deleteLater();
0262         KDirSelectDialog *dialog = new KDirSelectDialog(options()->initialDirectory());
0263         m_dialog = dialog;
0264         connect(dialog, &QDialog::accepted, this, &QPlatformDialogHelper::accept);
0265         connect(dialog, &QDialog::rejected, this, &QPlatformDialogHelper::reject);
0266         if (options()->isLabelExplicitlySet(QFileDialogOptions::Accept)) { // OK button
0267             dialog->setOkButtonText(options()->labelText(QFileDialogOptions::Accept));
0268         } else if (options()->isLabelExplicitlySet(QFileDialogOptions::Reject)) { // Cancel button
0269             dialog->setCancelButtonText(options()->labelText(QFileDialogOptions::Reject));
0270         } else if (options()->isLabelExplicitlySet(QFileDialogOptions::LookIn)) { // Location label
0271             // Not implemented yet.
0272         }
0273 
0274         if (!options()->windowTitle().isEmpty())
0275             m_dialog->setWindowTitle(options()->windowTitle());
0276     } else {
0277         // needed for accessing m_fileWidget
0278         KDEPlatformFileDialog *dialog = qobject_cast<KDEPlatformFileDialog *>(m_dialog);
0279         dialog->m_fileWidget->setOperationMode(options()->acceptMode() == QFileDialogOptions::AcceptOpen ? KFileWidget::Opening : KFileWidget::Saving);
0280         if (options()->windowTitle().isEmpty()) {
0281             dialog->setWindowTitle(options()->acceptMode() == QFileDialogOptions::AcceptOpen ? i18nc("@title:window", "Open File")
0282                                                                                              : i18nc("@title:window", "Save File"));
0283         } else {
0284             dialog->setWindowTitle(options()->windowTitle());
0285         }
0286         if (!m_directorySet) {
0287             setDirectory(options()->initialDirectory());
0288         }
0289         // dialog->setViewMode(options()->viewMode()); // don't override our options, fixes remembering the chosen view mode and sizes!
0290         dialog->setFileMode(options()->fileMode());
0291 
0292         // custom labels
0293         if (options()->isLabelExplicitlySet(QFileDialogOptions::Accept)) { // OK button
0294             dialog->setCustomLabel(QFileDialogOptions::Accept, options()->labelText(QFileDialogOptions::Accept));
0295         } else if (options()->isLabelExplicitlySet(QFileDialogOptions::Reject)) { // Cancel button
0296             dialog->setCustomLabel(QFileDialogOptions::Reject, options()->labelText(QFileDialogOptions::Reject));
0297         } else if (options()->isLabelExplicitlySet(QFileDialogOptions::LookIn)) { // Location label
0298             dialog->setCustomLabel(QFileDialogOptions::LookIn, options()->labelText(QFileDialogOptions::LookIn));
0299         }
0300 
0301         const QStringList mimeFilters = options()->mimeTypeFilters();
0302         const QStringList nameFilters = options()->nameFilters();
0303         if (!mimeFilters.isEmpty()) {
0304             QString defaultMimeFilter;
0305             if (options()->acceptMode() == QFileDialogOptions::AcceptSave) {
0306                 defaultMimeFilter = options()->initiallySelectedMimeTypeFilter();
0307                 if (defaultMimeFilter.isEmpty()) {
0308                     defaultMimeFilter = mimeFilters.at(0);
0309                 }
0310             }
0311             dialog->m_fileWidget->setMimeFilter(mimeFilters, defaultMimeFilter);
0312 
0313             if (mimeFilters.contains(QStringLiteral("inode/directory")))
0314                 dialog->m_fileWidget->setMode(dialog->m_fileWidget->mode() | KFile::Directory);
0315         } else if (!nameFilters.isEmpty()) {
0316             dialog->m_fileWidget->setFilter(qt2KdeFilter(nameFilters));
0317         }
0318 
0319         if (!options()->initiallySelectedMimeTypeFilter().isEmpty()) {
0320             selectMimeTypeFilter(options()->initiallySelectedMimeTypeFilter());
0321         } else if (!options()->initiallySelectedNameFilter().isEmpty()) {
0322             selectNameFilter(options()->initiallySelectedNameFilter());
0323         }
0324 
0325         // overwrite option
0326         if (options()->testOption(QFileDialogOptions::FileDialogOption::DontConfirmOverwrite)) {
0327             dialog->m_fileWidget->setConfirmOverwrite(false);
0328         } else if (options()->acceptMode() == QFileDialogOptions::AcceptSave) {
0329             dialog->m_fileWidget->setConfirmOverwrite(true);
0330         }
0331 
0332         const QStringList schemes = options()->supportedSchemes();
0333         dialog->m_fileWidget->setSupportedSchemes(schemes);
0334     }
0335 }
0336 
0337 void KDEPlatformFileDialogHelper::exec()
0338 {
0339     restoreSize();
0340     m_dialog->exec();
0341 }
0342 
0343 void KDEPlatformFileDialogHelper::hide()
0344 {
0345     m_dialog->hide();
0346 }
0347 
0348 void KDEPlatformFileDialogHelper::saveSize()
0349 {
0350     KSharedConfig::Ptr conf = KSharedConfig::openConfig();
0351     KConfigGroup group = conf->group("FileDialogSize");
0352     KWindowConfig::saveWindowSize(m_dialog->windowHandle(), group);
0353 }
0354 
0355 void KDEPlatformFileDialogHelper::restoreSize()
0356 {
0357     m_dialog->winId(); // ensure there's a window created
0358     KSharedConfig::Ptr conf = KSharedConfig::openConfig();
0359 
0360     // see the note below
0361     m_dialog->windowHandle()->resize(m_dialog->sizeHint());
0362 
0363     KWindowConfig::restoreWindowSize(m_dialog->windowHandle(), conf->group("FileDialogSize"));
0364     // NOTICE: QWindow::setGeometry() does NOT impact the backing QWidget geometry even if the platform
0365     // window was created -> QTBUG-40584. We therefore copy the size here.
0366     // TODO: remove once this was resolved in QWidget QPA
0367     m_dialog->resize(m_dialog->windowHandle()->size());
0368 }
0369 
0370 bool KDEPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::WindowModality windowModality, QWindow *parent)
0371 {
0372     initializeDialog();
0373     m_dialog->setWindowFlags(windowFlags);
0374     m_dialog->setWindowModality(windowModality);
0375     restoreSize();
0376     m_dialog->windowHandle()->setTransientParent(parent);
0377     m_dialog->show();
0378     return true;
0379 }
0380 
0381 QVariant KDEPlatformFileDialogHelper::styleHint(StyleHint hint) const
0382 {
0383     if (hint == DialogIsQtWindow) {
0384         return true;
0385     }
0386 
0387     return QPlatformDialogHelper::styleHint(hint);
0388 }
0389 
0390 QList<QUrl> KDEPlatformFileDialogHelper::selectedFiles() const
0391 {
0392     return m_dialog->selectedFiles();
0393 }
0394 
0395 QString KDEPlatformFileDialogHelper::selectedMimeTypeFilter() const
0396 {
0397     return m_dialog->selectedMimeTypeFilter();
0398 }
0399 
0400 void KDEPlatformFileDialogHelper::selectMimeTypeFilter(const QString &filter)
0401 {
0402     m_dialog->selectMimeTypeFilter(filter);
0403 }
0404 
0405 QString KDEPlatformFileDialogHelper::selectedNameFilter() const
0406 {
0407     return kde2QtFilter(options()->nameFilters(), m_dialog->selectedNameFilter(), m_dialog->currentFilterText());
0408 }
0409 
0410 QUrl KDEPlatformFileDialogHelper::directory() const
0411 {
0412     return m_dialog->directory();
0413 }
0414 
0415 void KDEPlatformFileDialogHelper::selectFile(const QUrl &filename)
0416 {
0417     m_dialog->selectFile(filename);
0418     m_fileSelected = true;
0419 }
0420 
0421 void KDEPlatformFileDialogHelper::setDirectory(const QUrl &directory)
0422 {
0423     if (!directory.isEmpty()) {
0424         m_dialog->setDirectory(directory);
0425         m_directorySet = true;
0426     }
0427 }
0428 
0429 void KDEPlatformFileDialogHelper::selectNameFilter(const QString &filter)
0430 {
0431     m_dialog->selectNameFilter(qt2KdeFilter(QStringList(filter)));
0432 }
0433 
0434 void KDEPlatformFileDialogHelper::setFilter()
0435 {
0436 }
0437 
0438 bool KDEPlatformFileDialogHelper::defaultNameFilterDisables() const
0439 {
0440     return false;
0441 }