File indexing completed on 2024-05-05 17:42:24

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