Warning, file /frameworks/kio/src/filewidgets/kfilewidget.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 // -*- c++ -*-
0002 /*
0003     This file is part of the KDE libraries
0004     SPDX-FileCopyrightText: 1997, 1998 Richard Moore <rich@kde.org>
0005     SPDX-FileCopyrightText: 1998 Stephan Kulow <coolo@kde.org>
0006     SPDX-FileCopyrightText: 1998 Daniel Grana <grana@ie.iwi.unibe.ch>
0007     SPDX-FileCopyrightText: 1999, 2000, 2001, 2002, 2003 Carsten Pfeiffer <pfeiffer@kde.org>
0008     SPDX-FileCopyrightText: 2003 Clarence Dang <dang@kde.org>
0009     SPDX-FileCopyrightText: 2007 David Faure <faure@kde.org>
0010     SPDX-FileCopyrightText: 2008 Rafael Fernández López <ereslibre@kde.org>
0011 
0012     SPDX-License-Identifier: LGPL-2.0-or-later
0013 */
0014 
0015 #include "kfilewidget.h"
0016 
0017 #include "../utils_p.h"
0018 #include "kfilebookmarkhandler_p.h"
0019 #include "kfileplacesmodel.h"
0020 #include "kfileplacesview.h"
0021 #include "kfilepreviewgenerator.h"
0022 #include "kfilewidgetdocktitlebar_p.h"
0023 #include "kurlcombobox.h"
0024 #include "kurlnavigator.h"
0025 
0026 #include <config-kiofilewidgets.h>
0027 
0028 #include <defaults-kfile.h>
0029 #include <kdiroperator.h>
0030 #include <kfilefiltercombo.h>
0031 #include <kfileitemdelegate.h>
0032 #include <kio/job.h>
0033 #include <kio/jobuidelegate.h>
0034 #include <kio/statjob.h>
0035 #include <kprotocolmanager.h>
0036 #include <krecentdirs.h>
0037 #include <krecentdocument.h>
0038 #include <kurlauthorized.h>
0039 #include <kurlcompletion.h>
0040 
0041 #include <KActionMenu>
0042 #include <KConfigGroup>
0043 #include <KDirLister>
0044 #include <KFileItem>
0045 #include <KFilePlacesModel>
0046 #include <KIconLoader>
0047 #include <KJobWidgets>
0048 #include <KLocalizedString>
0049 #include <KMessageBox>
0050 #include <KMessageWidget>
0051 #include <KSharedConfig>
0052 #include <KShell>
0053 #include <KStandardAction>
0054 
0055 #include <QAbstractProxyModel>
0056 #include <QApplication>
0057 #include <QCheckBox>
0058 #include <QDebug>
0059 #include <QDockWidget>
0060 #include <QFormLayout>
0061 #include <QHelpEvent>
0062 #include <QIcon>
0063 #include <QLabel>
0064 #include <QLayout>
0065 #include <QLineEdit>
0066 #include <QLoggingCategory>
0067 #include <QMenu>
0068 #include <QMimeDatabase>
0069 #include <QPushButton>
0070 #include <QScreen>
0071 #include <QSplitter>
0072 #include <QStandardPaths>
0073 #include <QTimer>
0074 #include <QToolBar>
0075 
0076 #include <algorithm>
0077 #include <array>
0078 
0079 Q_DECLARE_LOGGING_CATEGORY(KIO_KFILEWIDGETS_FW)
0080 Q_LOGGING_CATEGORY(KIO_KFILEWIDGETS_FW, "kf.kio.kfilewidgets.kfilewidget", QtInfoMsg)
0081 
0082 class KFileWidgetPrivate
0083 {
0084 public:
0085     explicit KFileWidgetPrivate(KFileWidget *qq)
0086         : q(qq)
0087     {
0088     }
0089 
0090     ~KFileWidgetPrivate()
0091     {
0092         delete m_bookmarkHandler; // Should be deleted before m_ops!
0093         // Must be deleted before m_ops, otherwise the unit test crashes due to the
0094         // connection to the QDockWidget::visibilityChanged signal, which may get
0095         // emitted after this object is destroyed
0096         delete m_placesDock;
0097         delete m_ops;
0098     }
0099 
0100     QSize screenSize() const
0101     {
0102         return q->parentWidget() ? q->parentWidget()->screen()->availableGeometry().size() //
0103                                  : QGuiApplication::primaryScreen()->availableGeometry().size();
0104     }
0105 
0106     void initDirOpWidgets();
0107     void initToolbar();
0108     void initZoomWidget();
0109     void initLocationWidget();
0110     void initFilterWidget();
0111     void updateLocationWhatsThis();
0112     void updateAutoSelectExtension();
0113     void initPlacesPanel();
0114     void setPlacesViewSplitterSizes();
0115     void initGUI();
0116     void readViewConfig();
0117     void writeViewConfig();
0118     void setNonExtSelection();
0119     void setLocationText(const QUrl &);
0120     void setLocationText(const QList<QUrl> &);
0121     void appendExtension(QUrl &url);
0122     void updateLocationEditExtension(const QString &);
0123     QString findMatchingFilter(const QString &filter, const QString &filename) const;
0124     void updateFilter();
0125     void updateFilterText();
0126     /**
0127      * Parses the string "line" for files. If line doesn't contain any ", the
0128      * whole line will be interpreted as one file. If the number of " is odd,
0129      * an empty list will be returned. Otherwise, all items enclosed in " "
0130      * will be returned as correct urls.
0131      */
0132     QList<QUrl> tokenize(const QString &line) const;
0133     /**
0134      * Reads the recent used files and inserts them into the location combobox
0135      */
0136     void readRecentFiles();
0137     /**
0138      * Saves the entries from the location combobox.
0139      */
0140     void saveRecentFiles();
0141     /**
0142      * called when an item is highlighted/selected in multiselection mode.
0143      * handles setting the m_locationEdit.
0144      */
0145     void multiSelectionChanged();
0146 
0147     /**
0148      * Returns the absolute version of the URL specified in m_locationEdit.
0149      */
0150     QUrl getCompleteUrl(const QString &) const;
0151 
0152     /**
0153      * Asks for overwrite confirmation using a KMessageBox and returns
0154      * true if the user accepts.
0155      *
0156      */
0157     bool toOverwrite(const QUrl &);
0158 
0159     // private slots
0160     void slotLocationChanged(const QString &);
0161     void urlEntered(const QUrl &);
0162     void enterUrl(const QUrl &);
0163     void enterUrl(const QString &);
0164     void locationAccepted(const QString &);
0165     void slotFilterChanged();
0166     void fileHighlighted(const KFileItem &);
0167     void fileSelected(const KFileItem &);
0168     void slotLoadingFinished();
0169     void togglePlacesPanel(bool show, QObject *sender = nullptr);
0170     void toggleBookmarks(bool);
0171     void slotAutoSelectExtClicked();
0172     void placesViewSplitterMoved(int, int);
0173     void activateUrlNavigator();
0174 
0175     enum ZoomState {
0176         ZoomOut,
0177         ZoomIn,
0178     };
0179     void changeIconsSize(ZoomState zoom);
0180     void slotDirOpIconSizeChanged(int size);
0181     void slotIconSizeSliderMoved(int);
0182     void slotIconSizeChanged(int);
0183     void slotViewDoubleClicked(const QModelIndex &);
0184     void slotViewKeyEnterReturnPressed();
0185 
0186     void addToRecentDocuments();
0187 
0188     QString locationEditCurrentText() const;
0189 
0190     /**
0191      * KIO::NetAccess::mostLocalUrl local replacement.
0192      * This method won't show any progress dialogs for stating, since
0193      * they are very annoying when stating.
0194      */
0195     QUrl mostLocalUrl(const QUrl &url);
0196 
0197     void setInlinePreviewShown(bool show);
0198 
0199     KFileWidget *const q;
0200 
0201     // the last selected url
0202     QUrl m_url;
0203 
0204     // now following all kind of widgets, that I need to rebuild
0205     // the geometry management
0206     QBoxLayout *m_boxLayout = nullptr;
0207     QFormLayout *m_lafBox = nullptr;
0208 
0209     QLabel *m_locationLabel = nullptr;
0210     QWidget *m_opsWidget = nullptr;
0211     QVBoxLayout *m_opsWidgetLayout = nullptr;
0212 
0213     QLabel *m_filterLabel = nullptr;
0214     KUrlNavigator *m_urlNavigator = nullptr;
0215     KMessageWidget *m_messageWidget = nullptr;
0216     QPushButton *m_okButton = nullptr;
0217     QPushButton *m_cancelButton = nullptr;
0218     QDockWidget *m_placesDock = nullptr;
0219     KFilePlacesView *m_placesView = nullptr;
0220     QSplitter *m_placesViewSplitter = nullptr;
0221     // caches the places view width. This value will be updated when the splitter
0222     // is moved. This allows us to properly set a value when the dialog itself
0223     // is resized
0224     int m_placesViewWidth = -1;
0225 
0226     QWidget *m_labeledCustomWidget = nullptr;
0227     QWidget *m_bottomCustomWidget = nullptr;
0228 
0229     // Automatically Select Extension stuff
0230     QCheckBox *m_autoSelectExtCheckBox = nullptr;
0231     QString m_extension; // current extension for this filter
0232 
0233     QList<QUrl> m_urlList; // the list of selected urls
0234 
0235     KFileWidget::OperationMode m_operationMode = KFileWidget::Opening;
0236 
0237     // The file class used for KRecentDirs
0238     QString m_fileClass;
0239 
0240     KFileBookmarkHandler *m_bookmarkHandler = nullptr;
0241 
0242     KActionMenu *m_bookmarkButton = nullptr;
0243 
0244     QToolBar *m_toolbar = nullptr;
0245     KUrlComboBox *m_locationEdit = nullptr;
0246     KDirOperator *m_ops = nullptr;
0247     KFileFilterCombo *m_filterWidget = nullptr;
0248     QTimer m_filterDelayTimer;
0249 
0250     KFilePlacesModel *m_model = nullptr;
0251 
0252     // whether or not the _user_ has checked the above box
0253     bool m_autoSelectExtChecked = false;
0254 
0255     // indicates if the location edit should be kept or cleared when changing
0256     // directories
0257     bool m_keepLocation = false;
0258 
0259     // the KDirOperators view is set in KFileWidget::show(), so to avoid
0260     // setting it again and again, we have this nice little boolean :)
0261     bool m_hasView = false;
0262 
0263     bool m_hasDefaultFilter = false; // necessary for the m_operationMode
0264     bool m_inAccept = false; // true between beginning and end of accept()
0265     bool m_confirmOverwrite = false;
0266     bool m_differentHierarchyLevelItemsEntered = false;
0267 
0268     const std::array<short, 8> m_stdIconSizes = {
0269         KIconLoader::SizeSmall,
0270         KIconLoader::SizeSmallMedium,
0271         KIconLoader::SizeMedium,
0272         KIconLoader::SizeLarge,
0273         KIconLoader::SizeHuge,
0274         KIconLoader::SizeEnormous,
0275         256,
0276         512,
0277     };
0278 
0279     QSlider *m_iconSizeSlider = nullptr;
0280     QAction *m_zoomOutAction = nullptr;
0281     QAction *m_zoomInAction = nullptr;
0282 
0283     // The group which stores app-specific settings. These settings are recent
0284     // files and urls. Visual settings (view mode, sorting criteria...) are not
0285     // app-specific and are stored in kdeglobals
0286     KConfigGroup m_configGroup;
0287 
0288     KToggleAction *m_toggleBookmarksAction = nullptr;
0289     KToggleAction *m_togglePlacesPanelAction = nullptr;
0290 };
0291 
0292 Q_GLOBAL_STATIC(QUrl, lastDirectory) // to set the start path
0293 
0294 // returns true if the string contains "<a>:/" sequence, where <a> is at least 2 alpha chars
0295 static bool containsProtocolSection(const QString &string)
0296 {
0297     int len = string.length();
0298     static const char prot[] = ":/";
0299     for (int i = 0; i < len;) {
0300         i = string.indexOf(QLatin1String(prot), i);
0301         if (i == -1) {
0302             return false;
0303         }
0304         int j = i - 1;
0305         for (; j >= 0; j--) {
0306             const QChar &ch(string[j]);
0307             if (ch.toLatin1() == 0 || !ch.isLetter()) {
0308                 break;
0309             }
0310             if (ch.isSpace() && (i - j - 1) >= 2) {
0311                 return true;
0312             }
0313         }
0314         if (j < 0 && i >= 2) {
0315             return true; // at least two letters before ":/"
0316         }
0317         i += 3; // skip : and / and one char
0318     }
0319     return false;
0320 }
0321 
0322 // this string-to-url conversion function handles relative paths, full paths and URLs
0323 // without the http-prepending that QUrl::fromUserInput does.
0324 static QUrl urlFromString(const QString &str)
0325 {
0326     if (Utils::isAbsoluteLocalPath(str)) {
0327         return QUrl::fromLocalFile(str);
0328     }
0329     QUrl url(str);
0330     if (url.isRelative()) {
0331         url.clear();
0332         url.setPath(str);
0333     }
0334     return url;
0335 }
0336 
0337 KFileWidget::KFileWidget(const QUrl &_startDir, QWidget *parent)
0338     : QWidget(parent)
0339     , d(new KFileWidgetPrivate(this))
0340 {
0341     QUrl startDir(_startDir);
0342     // qDebug() << "startDir" << startDir;
0343     QString filename;
0344 
0345     d->m_okButton = new QPushButton(this);
0346     KGuiItem::assign(d->m_okButton, KStandardGuiItem::ok());
0347     d->m_okButton->setDefault(true);
0348     d->m_cancelButton = new QPushButton(this);
0349     KGuiItem::assign(d->m_cancelButton, KStandardGuiItem::cancel());
0350     // The dialog shows them
0351     d->m_okButton->hide();
0352     d->m_cancelButton->hide();
0353 
0354     d->initDirOpWidgets();
0355 
0356     // Resolve this now so that a 'kfiledialog:' URL, if specified,
0357     // does not get inserted into the urlNavigator history.
0358     d->m_url = getStartUrl(startDir, d->m_fileClass, filename);
0359     startDir = d->m_url;
0360 
0361     const auto operatorActions = d->m_ops->allActions();
0362     for (QAction *action : operatorActions) {
0363         addAction(action);
0364     }
0365 
0366     QAction *goToNavigatorAction = new QAction(this);
0367 
0368     connect(goToNavigatorAction, &QAction::triggered, this, [this]() {
0369         d->activateUrlNavigator();
0370     });
0371 
0372     goToNavigatorAction->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_L));
0373 
0374     addAction(goToNavigatorAction);
0375 
0376     KUrlComboBox *pathCombo = d->m_urlNavigator->editor();
0377     KUrlCompletion *pathCompletionObj = new KUrlCompletion(KUrlCompletion::DirCompletion);
0378     pathCombo->setCompletionObject(pathCompletionObj);
0379     pathCombo->setAutoDeleteCompletionObject(true);
0380 
0381     connect(d->m_urlNavigator, &KUrlNavigator::urlChanged, this, [this](const QUrl &url) {
0382         d->enterUrl(url);
0383     });
0384     connect(d->m_urlNavigator, &KUrlNavigator::returnPressed, d->m_ops, qOverload<>(&QWidget::setFocus));
0385 
0386     // Location, "Name:", line-edit and label
0387     d->initLocationWidget();
0388 
0389     // "Filter:" line-edit and label
0390     d->initFilterWidget();
0391 
0392     // the Automatically Select Extension checkbox
0393     // (the text, visibility etc. is set in updateAutoSelectExtension(), which is called by readConfig())
0394     d->m_autoSelectExtCheckBox = new QCheckBox(this);
0395     connect(d->m_autoSelectExtCheckBox, &QCheckBox::clicked, this, [this]() {
0396         d->slotAutoSelectExtClicked();
0397     });
0398 
0399     d->initGUI(); // activate GM
0400 
0401     // read our configuration
0402     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0403     config->reparseConfiguration(); // grab newly added dirs by other processes (#403524)
0404     KConfigGroup group(config, ConfigGroup);
0405     readConfig(group);
0406 
0407     d->m_ops->action(KDirOperator::ShowPreview)->setChecked(d->m_ops->isInlinePreviewShown());
0408     d->slotDirOpIconSizeChanged(d->m_ops->iconSize());
0409 
0410     KFilePreviewGenerator *pg = d->m_ops->previewGenerator();
0411     if (pg) {
0412         d->m_ops->action(KDirOperator::ShowPreview)->setChecked(pg->isPreviewShown());
0413     }
0414 
0415     // getStartUrl() above will have resolved the startDir parameter into
0416     // a directory and file name in the two cases: (a) where it is a
0417     // special "kfiledialog:" URL, or (b) where it is a plain file name
0418     // only without directory or protocol.  For any other startDir
0419     // specified, it is not possible to resolve whether there is a file name
0420     // present just by looking at the URL; the only way to be sure is
0421     // to stat it.
0422     bool statRes = false;
0423     if (filename.isEmpty()) {
0424         KIO::StatJob *statJob = KIO::stat(startDir, KIO::HideProgressInfo);
0425         KJobWidgets::setWindow(statJob, this);
0426         statRes = statJob->exec();
0427         // qDebug() << "stat of" << startDir << "-> statRes" << statRes << "isDir" << statJob->statResult().isDir();
0428         if (!statRes || !statJob->statResult().isDir()) {
0429             filename = startDir.fileName();
0430             startDir = startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
0431             // qDebug() << "statJob -> startDir" << startDir << "filename" << filename;
0432         }
0433     }
0434 
0435     d->m_ops->setUrl(startDir, true);
0436     d->m_urlNavigator->setLocationUrl(startDir);
0437     if (d->m_placesView) {
0438         d->m_placesView->setUrl(startDir);
0439     }
0440 
0441     // We have a file name either explicitly specified, or have checked that
0442     // we could stat it and it is not a directory.  Set it.
0443     if (!filename.isEmpty()) {
0444         QLineEdit *lineEdit = d->m_locationEdit->lineEdit();
0445         // qDebug() << "selecting filename" << filename;
0446         if (statRes) {
0447             d->setLocationText(QUrl(filename));
0448         } else {
0449             lineEdit->setText(filename);
0450             // Preserve this filename when clicking on the view (cf fileHighlighted)
0451             lineEdit->setModified(true);
0452         }
0453         lineEdit->selectAll();
0454     }
0455 
0456     d->m_locationEdit->setFocus();
0457 
0458     const QAction *showHiddenAction = d->m_ops->action(KDirOperator::ShowHiddenFiles);
0459     Q_ASSERT(showHiddenAction);
0460     d->m_urlNavigator->setShowHiddenFolders(showHiddenAction->isChecked());
0461     connect(showHiddenAction, &QAction::toggled, this, [this](bool checked) {
0462         d->m_urlNavigator->setShowHiddenFolders(checked);
0463     });
0464 
0465     const QAction *hiddenFilesLastAction = d->m_ops->action(KDirOperator::SortHiddenFilesLast);
0466     Q_ASSERT(hiddenFilesLastAction);
0467     d->m_urlNavigator->setSortHiddenFoldersLast(hiddenFilesLastAction->isChecked());
0468     connect(hiddenFilesLastAction, &QAction::toggled, this, [this](bool checked) {
0469         d->m_urlNavigator->setSortHiddenFoldersLast(checked);
0470     });
0471 }
0472 
0473 KFileWidget::~KFileWidget()
0474 {
0475     KSharedConfig::Ptr config = KSharedConfig::openConfig();
0476     config->sync();
0477     d->m_ops->removeEventFilter(this);
0478     d->m_locationEdit->removeEventFilter(this);
0479 }
0480 
0481 void KFileWidget::setLocationLabel(const QString &text)
0482 {
0483     d->m_locationLabel->setText(text);
0484 }
0485 
0486 void KFileWidget::setFilters(const QList<KFileFilter> &filters, const KFileFilter &activeFilter)
0487 {
0488     d->m_ops->clearFilter();
0489     d->m_filterWidget->setFilters(filters, activeFilter);
0490     d->m_ops->updateDir();
0491     d->m_hasDefaultFilter = false;
0492     d->m_filterWidget->setEditable(true);
0493     d->updateFilterText();
0494 
0495     d->updateAutoSelectExtension();
0496 }
0497 
0498 KFileFilter KFileWidget::currentFilter() const
0499 {
0500     return d->m_filterWidget->currentFilter();
0501 }
0502 
0503 void KFileWidget::clearFilter()
0504 {
0505     d->m_filterWidget->setFilters({}, KFileFilter());
0506     d->m_ops->clearFilter();
0507     d->m_hasDefaultFilter = false;
0508     d->m_filterWidget->setEditable(true);
0509 
0510     d->updateAutoSelectExtension();
0511 }
0512 
0513 void KFileWidget::setPreviewWidget(KPreviewWidgetBase *w)
0514 {
0515     d->m_ops->setPreviewWidget(w);
0516     d->m_ops->clearHistory();
0517     d->m_hasView = true;
0518 }
0519 
0520 QUrl KFileWidgetPrivate::getCompleteUrl(const QString &_url) const
0521 {
0522     //     qDebug() << "got url " << _url;
0523 
0524     const QString url = KShell::tildeExpand(_url);
0525     QUrl u;
0526 
0527     if (Utils::isAbsoluteLocalPath(url)) {
0528         u = QUrl::fromLocalFile(url);
0529     } else {
0530         QUrl relativeUrlTest(m_ops->url());
0531         relativeUrlTest.setPath(Utils::concatPaths(relativeUrlTest.path(), url));
0532         if (!m_ops->dirLister()->findByUrl(relativeUrlTest).isNull() || !KProtocolInfo::isKnownProtocol(relativeUrlTest)) {
0533             u = relativeUrlTest;
0534         } else {
0535             // Try to preserve URLs if they have a scheme (for example,
0536             // "https://example.com/foo.txt") and otherwise resolve relative
0537             // paths to absolute ones (e.g. "foo.txt" -> "file:///tmp/foo.txt").
0538             u = QUrl(url);
0539             if (u.isRelative()) {
0540                 u = relativeUrlTest;
0541             }
0542         }
0543     }
0544 
0545     return u;
0546 }
0547 
0548 QSize KFileWidget::sizeHint() const
0549 {
0550     int fontSize = fontMetrics().height();
0551     const QSize goodSize(48 * fontSize, 30 * fontSize);
0552     const QSize scrnSize = d->screenSize();
0553     const QSize minSize(scrnSize / 2);
0554     const QSize maxSize(scrnSize * qreal(0.9));
0555     return (goodSize.expandedTo(minSize).boundedTo(maxSize));
0556 }
0557 
0558 static QString relativePathOrUrl(const QUrl &baseUrl, const QUrl &url);
0559 
0560 /**
0561  * Escape the given Url so that is fit for use in the selected list of file. This
0562  * mainly handles double quote (") characters. These are used to separate entries
0563  * in the list, however, if `"` appears in the filename (or path), this will be
0564  * escaped as `\"`. Later, the tokenizer is able to understand the difference
0565  * and do the right thing
0566  */
0567 static QString escapeDoubleQuotes(QString &&path);
0568 
0569 // Called by KFileDialog
0570 void KFileWidget::slotOk()
0571 {
0572     //     qDebug() << "slotOk\n";
0573 
0574     const QString locationEditCurrentText(KShell::tildeExpand(d->locationEditCurrentText()));
0575 
0576     QList<QUrl> locationEditCurrentTextList(d->tokenize(locationEditCurrentText));
0577     KFile::Modes mode = d->m_ops->mode();
0578 
0579     // if there is nothing to do, just return from here
0580     if (locationEditCurrentTextList.isEmpty()) {
0581         return;
0582     }
0583 
0584     // Make sure that one of the modes was provided
0585     if (!((mode & KFile::File) || (mode & KFile::Directory) || (mode & KFile::Files))) {
0586         mode |= KFile::File;
0587         // qDebug() << "No mode() provided";
0588     }
0589 
0590     // Clear the list as we are going to refill it
0591     d->m_urlList.clear();
0592 
0593     const bool directoryMode = (mode & KFile::Directory);
0594     const bool onlyDirectoryMode = directoryMode && !(mode & KFile::File) && !(mode & KFile::Files);
0595 
0596     // if we are on file mode, and the list of provided files/folder is greater than one, inform
0597     // the user about it
0598     if (locationEditCurrentTextList.count() > 1) {
0599         if (mode & KFile::File) {
0600             KMessageBox::error(this, i18n("You can only select one file"), i18n("More than one file provided"));
0601             return;
0602         }
0603 
0604         /**
0605          * Logic of the next part of code (ends at "end multi relative urls").
0606          *
0607          * We allow for instance to be at "/" and insert '"home/foo/bar.txt" "boot/grub/menu.lst"'.
0608          * Why we need to support this ? Because we provide tree views, which aren't plain.
0609          *
0610          * Now, how does this logic work. It will get the first element on the list (with no filename),
0611          * following the previous example say "/home/foo" and set it as the top most url.
0612          *
0613          * After this, it will iterate over the rest of items and check if this URL (topmost url)
0614          * contains the url being iterated.
0615          *
0616          * As you might have guessed it will do "/home/foo" against "/boot/grub" (again stripping
0617          * filename), and a false will be returned. Then we upUrl the top most url, resulting in
0618          * "/home" against "/boot/grub", what will again return false, so we upUrl again. Now we
0619          * have "/" against "/boot/grub", what returns true for us, so we can say that the closest
0620          * common ancestor of both is "/".
0621          *
0622          * This example has been written for 2 urls, but this works for any number of urls.
0623          */
0624         if (!d->m_differentHierarchyLevelItemsEntered) { // avoid infinite recursion. running this
0625             int start = 0;
0626             QUrl topMostUrl;
0627             KIO::StatJob *statJob = nullptr;
0628             bool res = false;
0629 
0630             // we need to check for a valid first url, so in theory we only iterate one time over
0631             // this loop. However it can happen that the user did
0632             // "home/foo/nonexistantfile" "boot/grub/menu.lst", so we look for a good first
0633             // candidate.
0634             while (!res && start < locationEditCurrentTextList.count()) {
0635                 topMostUrl = locationEditCurrentTextList.at(start);
0636                 statJob = KIO::stat(topMostUrl, KIO::HideProgressInfo);
0637                 KJobWidgets::setWindow(statJob, this);
0638                 res = statJob->exec();
0639                 start++;
0640             }
0641 
0642             Q_ASSERT(statJob);
0643 
0644             // if this is not a dir, strip the filename. after this we have an existent and valid
0645             // dir (we stated correctly the file).
0646             if (!statJob->statResult().isDir()) {
0647                 topMostUrl = topMostUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
0648             }
0649 
0650             // now the funny part. for the rest of filenames, go and look for the closest ancestor
0651             // of all them.
0652             for (int i = start; i < locationEditCurrentTextList.count(); ++i) {
0653                 QUrl currUrl = locationEditCurrentTextList.at(i);
0654                 KIO::StatJob *statJob = KIO::stat(currUrl, KIO::HideProgressInfo);
0655                 KJobWidgets::setWindow(statJob, this);
0656                 int res = statJob->exec();
0657                 if (res) {
0658                     // again, we don't care about filenames
0659                     if (!statJob->statResult().isDir()) {
0660                         currUrl = currUrl.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash);
0661                     }
0662 
0663                     // iterate while this item is contained on the top most url
0664                     while (!topMostUrl.matches(currUrl, QUrl::StripTrailingSlash) && !topMostUrl.isParentOf(currUrl)) {
0665                         topMostUrl = KIO::upUrl(topMostUrl);
0666                     }
0667                 }
0668             }
0669 
0670             // now recalculate all paths for them being relative in base of the top most url
0671             QStringList stringList;
0672             stringList.reserve(locationEditCurrentTextList.count());
0673             for (int i = 0; i < locationEditCurrentTextList.count(); ++i) {
0674                 Q_ASSERT(topMostUrl.isParentOf(locationEditCurrentTextList[i]));
0675                 QString relativePath = relativePathOrUrl(topMostUrl, locationEditCurrentTextList[i]);
0676                 stringList << escapeDoubleQuotes(std::move(relativePath));
0677             }
0678 
0679             d->m_ops->setUrl(topMostUrl, true);
0680             const bool signalsBlocked = d->m_locationEdit->lineEdit()->blockSignals(true);
0681             d->m_locationEdit->lineEdit()->setText(QStringLiteral("\"%1\"").arg(stringList.join(QStringLiteral("\" \""))));
0682             d->m_locationEdit->lineEdit()->blockSignals(signalsBlocked);
0683 
0684             d->m_differentHierarchyLevelItemsEntered = true;
0685             slotOk();
0686             return;
0687         }
0688         /**
0689          * end multi relative urls
0690          */
0691     } else if (!locationEditCurrentTextList.isEmpty()) {
0692         // if we are on file or files mode, and we have an absolute url written by
0693         // the user:
0694         //  * convert it to relative and call slotOk again if the protocol supports listing.
0695         //  * use the full url if the protocol doesn't support listing
0696         // This is because when using a protocol that supports listing we want to show the directory
0697         // the user just opened/saved from the next time they open the dialog, it makes sense usability wise.
0698         // If the protocol doesn't support listing (i.e. http:// ) the user would end up with the dialog
0699         // showing an "empty directory" which is bad usability wise.
0700         if (!locationEditCurrentText.isEmpty() && !onlyDirectoryMode
0701             && (Utils::isAbsoluteLocalPath(locationEditCurrentText) || containsProtocolSection(locationEditCurrentText))) {
0702             QUrl url = urlFromString(locationEditCurrentText);
0703             if (KProtocolManager::supportsListing(url)) {
0704                 QString fileName;
0705                 if (d->m_operationMode == Opening) {
0706                     KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo);
0707                     KJobWidgets::setWindow(statJob, this);
0708                     int res = statJob->exec();
0709                     if (res) {
0710                         if (!statJob->statResult().isDir()) {
0711                             fileName = url.fileName();
0712                             url = url.adjusted(QUrl::RemoveFilename); // keeps trailing slash
0713                         } else {
0714                             Utils::appendSlashToPath(url);
0715                         }
0716                     }
0717                 } else {
0718                     const QUrl directory = url.adjusted(QUrl::RemoveFilename);
0719                     // Check if the folder exists
0720                     KIO::StatJob *statJob = KIO::stat(directory, KIO::HideProgressInfo);
0721                     KJobWidgets::setWindow(statJob, this);
0722                     int res = statJob->exec();
0723                     if (res) {
0724                         if (statJob->statResult().isDir()) {
0725                             url = url.adjusted(QUrl::StripTrailingSlash);
0726                             fileName = url.fileName();
0727                             url = url.adjusted(QUrl::RemoveFilename);
0728                         }
0729                     }
0730                 }
0731                 d->m_ops->setUrl(url, true);
0732                 const bool signalsBlocked = d->m_locationEdit->lineEdit()->blockSignals(true);
0733                 d->m_locationEdit->lineEdit()->setText(fileName);
0734                 d->m_locationEdit->lineEdit()->blockSignals(signalsBlocked);
0735                 slotOk();
0736                 return;
0737             } else {
0738                 locationEditCurrentTextList = {url};
0739             }
0740         }
0741     }
0742 
0743     // restore it
0744     d->m_differentHierarchyLevelItemsEntered = false;
0745 
0746     // locationEditCurrentTextList contains absolute paths
0747     // this is the general loop for the File and Files mode. Obviously we know
0748     // that the File mode will iterate only one time here
0749     QList<QUrl>::ConstIterator it = locationEditCurrentTextList.constBegin();
0750     bool filesInList = false;
0751     while (it != locationEditCurrentTextList.constEnd()) {
0752         QUrl url(*it);
0753 
0754         if (d->m_operationMode == Saving && !directoryMode) {
0755             d->appendExtension(url);
0756         }
0757 
0758         d->m_url = url;
0759         KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo);
0760         KJobWidgets::setWindow(statJob, this);
0761         int res = statJob->exec();
0762 
0763         if (!KUrlAuthorized::authorizeUrlAction(QStringLiteral("open"), QUrl(), url)) {
0764             QString msg = KIO::buildErrorString(KIO::ERR_ACCESS_DENIED, d->m_url.toDisplayString());
0765             KMessageBox::error(this, msg);
0766             return;
0767         }
0768 
0769         // if we are on local mode, make sure we haven't got a remote base url
0770         if ((mode & KFile::LocalOnly) && !d->mostLocalUrl(d->m_url).isLocalFile()) {
0771             KMessageBox::error(this, i18n("You can only select local files"), i18n("Remote files not accepted"));
0772             return;
0773         }
0774 
0775         const auto &supportedSchemes = d->m_model->supportedSchemes();
0776         if (!supportedSchemes.isEmpty() && !supportedSchemes.contains(d->m_url.scheme())) {
0777             KMessageBox::error(this,
0778                                i18np("The selected URL uses an unsupported scheme. "
0779                                      "Please use the following scheme: %2",
0780                                      "The selected URL uses an unsupported scheme. "
0781                                      "Please use one of the following schemes: %2",
0782                                      supportedSchemes.size(),
0783                                      supportedSchemes.join(QLatin1String(", "))),
0784                                i18n("Unsupported URL scheme"));
0785             return;
0786         }
0787 
0788         // if we are given a folder when not on directory mode, let's get into it
0789         if (res && !directoryMode && statJob->statResult().isDir()) {
0790             // check if we were given more than one folder, in that case we don't know to which one
0791             // cd
0792             ++it;
0793             while (it != locationEditCurrentTextList.constEnd()) {
0794                 QUrl checkUrl(*it);
0795                 KIO::StatJob *checkStatJob = KIO::stat(checkUrl, KIO::HideProgressInfo);
0796                 KJobWidgets::setWindow(checkStatJob, this);
0797                 bool res = checkStatJob->exec();
0798                 if (res && checkStatJob->statResult().isDir()) {
0799                     KMessageBox::error(this,
0800                                        i18n("More than one folder has been selected and this dialog does not accept folders, so it is not possible to decide "
0801                                             "which one to enter. Please select only one folder to list it."),
0802                                        i18n("More than one folder provided"));
0803                     return;
0804                 } else if (res) {
0805                     filesInList = true;
0806                 }
0807                 ++it;
0808             }
0809             if (filesInList) {
0810                 KMessageBox::information(
0811                     this,
0812                     i18n("At least one folder and one file has been selected. Selected files will be ignored and the selected folder will be listed"),
0813                     i18n("Files and folders selected"));
0814             }
0815             d->m_ops->setUrl(url, true);
0816             const bool signalsBlocked = d->m_locationEdit->lineEdit()->blockSignals(true);
0817             d->m_locationEdit->lineEdit()->setText(QString());
0818             d->m_locationEdit->lineEdit()->blockSignals(signalsBlocked);
0819             return;
0820         } else if (res && onlyDirectoryMode && !statJob->statResult().isDir()) {
0821             // if we are given a file when on directory only mode, reject it
0822             return;
0823         } else if (!(mode & KFile::ExistingOnly) || res) {
0824             // if we don't care about ExistingOnly flag, add the file even if
0825             // it doesn't exist. If we care about it, don't add it to the list
0826             if (!onlyDirectoryMode || (res && statJob->statResult().isDir())) {
0827                 d->m_urlList << url;
0828             }
0829             filesInList = true;
0830         } else {
0831             KMessageBox::error(this, i18n("The file \"%1\" could not be found", url.toDisplayString(QUrl::PreferLocalFile)), i18n("Cannot open file"));
0832             return; // do not emit accepted() if we had ExistingOnly flag and stat failed
0833         }
0834 
0835         if ((d->m_operationMode == Saving) && d->m_confirmOverwrite && !d->toOverwrite(url)) {
0836             return;
0837         }
0838 
0839         ++it;
0840     }
0841 
0842     // if we have reached this point and we didn't return before, that is because
0843     // we want this dialog to be accepted
0844     Q_EMIT accepted();
0845 }
0846 
0847 void KFileWidget::accept()
0848 {
0849     d->m_inAccept = true;
0850 
0851     *lastDirectory() = d->m_ops->url();
0852     if (!d->m_fileClass.isEmpty()) {
0853         KRecentDirs::add(d->m_fileClass, d->m_ops->url().toString());
0854     }
0855 
0856     // clear the topmost item, we insert it as full path later on as item 1
0857     d->m_locationEdit->setItemText(0, QString());
0858 
0859     const QList<QUrl> list = selectedUrls();
0860     int atmost = d->m_locationEdit->maxItems(); // don't add more items than necessary
0861     for (const auto &url : list) {
0862         if (atmost-- == 0) {
0863             break;
0864         }
0865 
0866         // we strip the last slash (-1) because KUrlComboBox does that as well
0867         // when operating in file-mode. If we wouldn't , dupe-finding wouldn't
0868         // work.
0869         const QString file = url.toDisplayString(QUrl::StripTrailingSlash | QUrl::PreferLocalFile);
0870 
0871         // remove dupes
0872         for (int i = 1; i < d->m_locationEdit->count(); ++i) {
0873             if (d->m_locationEdit->itemText(i) == file) {
0874                 d->m_locationEdit->removeItem(i--);
0875                 break;
0876             }
0877         }
0878         // FIXME I don't think this works correctly when the KUrlComboBox has some default urls.
0879         // KUrlComboBox should provide a function to add an url and rotate the existing ones, keeping
0880         // track of maxItems, and we shouldn't be able to insert items as we please.
0881         d->m_locationEdit->insertItem(1, file);
0882     }
0883 
0884     d->writeViewConfig();
0885     d->saveRecentFiles();
0886 
0887     d->addToRecentDocuments();
0888 
0889     if (!(mode() & KFile::Files)) { // single selection
0890         Q_EMIT fileSelected(d->m_url);
0891     }
0892 
0893     d->m_ops->close();
0894 }
0895 
0896 void KFileWidgetPrivate::fileHighlighted(const KFileItem &i)
0897 {
0898     if ((m_locationEdit->hasFocus() && !m_locationEdit->currentText().isEmpty())) { // don't disturb
0899         return;
0900     }
0901 
0902     if (!i.isNull() && i.isDir() && !(m_ops->mode() & KFile::Directory)) {
0903         return;
0904     }
0905 
0906     const bool modified = m_locationEdit->lineEdit()->isModified();
0907 
0908     if (!(m_ops->mode() & KFile::Files)) {
0909         if (i.isNull()) {
0910             if (!modified) {
0911                 setLocationText(QUrl());
0912             }
0913             return;
0914         }
0915 
0916         m_url = i.url();
0917 
0918         if (!m_locationEdit->hasFocus()) { // don't disturb while editing
0919             setLocationText(m_url);
0920         }
0921 
0922         Q_EMIT q->fileHighlighted(m_url);
0923     } else {
0924         multiSelectionChanged();
0925         Q_EMIT q->selectionChanged();
0926     }
0927 
0928     m_locationEdit->lineEdit()->setModified(false);
0929 
0930     // When saving, and when double-click mode is being used, highlight the
0931     // filename after a file is single-clicked so the user has a chance to quickly
0932     // rename it if desired
0933     // Note that double-clicking will override this and overwrite regardless of
0934     // single/double click mouse setting (see slotViewDoubleClicked() )
0935     if (m_operationMode == KFileWidget::Saving) {
0936         m_locationEdit->setFocus();
0937     }
0938 }
0939 
0940 void KFileWidgetPrivate::fileSelected(const KFileItem &i)
0941 {
0942     if (!i.isNull() && i.isDir()) {
0943         return;
0944     }
0945 
0946     if (!(m_ops->mode() & KFile::Files)) {
0947         if (i.isNull()) {
0948             setLocationText(QUrl());
0949             return;
0950         }
0951         setLocationText(i.targetUrl());
0952     } else {
0953         multiSelectionChanged();
0954         Q_EMIT q->selectionChanged();
0955     }
0956 
0957     // Same as above in fileHighlighted(), but for single-click mode
0958     if (m_operationMode == KFileWidget::Saving) {
0959         m_locationEdit->setFocus();
0960     } else {
0961         q->slotOk();
0962     }
0963 }
0964 
0965 // I know it's slow to always iterate thru the whole filelist
0966 // (d->m_ops->selectedItems()), but what can we do?
0967 void KFileWidgetPrivate::multiSelectionChanged()
0968 {
0969     if (m_locationEdit->hasFocus() && !m_locationEdit->currentText().isEmpty()) { // don't disturb
0970         return;
0971     }
0972 
0973     const KFileItemList list = m_ops->selectedItems();
0974 
0975     if (list.isEmpty()) {
0976         setLocationText(QUrl());
0977         return;
0978     }
0979 
0980     setLocationText(list.targetUrlList());
0981 }
0982 
0983 void KFileWidgetPrivate::setLocationText(const QUrl &url)
0984 {
0985     // Block m_locationEdit signals as setCurrentItem() will cause textChanged() to get
0986     // emitted, so slotLocationChanged() will be called. Make sure we don't clear the
0987     // KDirOperator's view-selection in there
0988     const QSignalBlocker blocker(m_locationEdit);
0989 
0990     if (!url.isEmpty()) {
0991         if (!url.isRelative()) {
0992             const QUrl directory = url.adjusted(QUrl::RemoveFilename);
0993             if (!directory.path().isEmpty()) {
0994                 q->setUrl(directory, false);
0995             } else {
0996                 q->setUrl(url, false);
0997             }
0998         }
0999         m_locationEdit->setEditText(escapeDoubleQuotes(url.fileName()));
1000     } else if (!m_locationEdit->lineEdit()->text().isEmpty()) {
1001         m_locationEdit->clearEditText();
1002     }
1003 
1004     if (m_operationMode == KFileWidget::Saving) {
1005         setNonExtSelection();
1006     }
1007 }
1008 
1009 static QString relativePathOrUrl(const QUrl &baseUrl, const QUrl &url)
1010 {
1011     if (baseUrl.isParentOf(url)) {
1012         const QString basePath(QDir::cleanPath(baseUrl.path()));
1013         QString relPath(QDir::cleanPath(url.path()));
1014         relPath.remove(0, basePath.length());
1015         if (relPath.startsWith(QLatin1Char('/'))) {
1016             relPath.remove(0, 1);
1017         }
1018         return relPath;
1019     } else {
1020         return url.toDisplayString();
1021     }
1022 }
1023 
1024 static QString escapeDoubleQuotes(QString &&path)
1025 {
1026     // First escape the escape character that we are using
1027     path.replace(QStringLiteral("\\"), QStringLiteral("\\\\"));
1028     // Second, escape the quotes
1029     path.replace(QStringLiteral("\""), QStringLiteral("\\\""));
1030     return path;
1031 }
1032 
1033 void KFileWidgetPrivate::initDirOpWidgets()
1034 {
1035     m_opsWidget = new QWidget(q);
1036     m_opsWidgetLayout = new QVBoxLayout(m_opsWidget);
1037     m_opsWidgetLayout->setContentsMargins(0, 0, 0, 0);
1038     m_opsWidgetLayout->setSpacing(0);
1039 
1040     m_model = new KFilePlacesModel(q);
1041 
1042     // Don't pass "startDir" (KFileWidget constructor 1st arg) to the
1043     // KUrlNavigator at this stage: it may also contain a file name which
1044     // should not get inserted in that form into the old-style navigation
1045     // bar history. Wait until the KIO::stat has been done later.
1046     //
1047     // The stat cannot be done before this point, bug 172678.
1048     m_urlNavigator = new KUrlNavigator(m_model, QUrl(), m_opsWidget); // d->m_toolbar);
1049     m_urlNavigator->setPlacesSelectorVisible(false);
1050 
1051     m_urlNavigator->setContentsMargins(q->style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
1052                                        0,
1053                                        q->style()->pixelMetric(QStyle::PM_LayoutRightMargin),
1054                                        q->style()->pixelMetric(QStyle::PM_LayoutBottomMargin) - 2);
1055 
1056     m_messageWidget = new KMessageWidget(q);
1057     m_messageWidget->setMessageType(KMessageWidget::Error);
1058     m_messageWidget->hide();
1059 
1060     auto topSeparator = new QFrame(q);
1061     topSeparator->setFrameStyle(QFrame::HLine);
1062 
1063     m_ops = new KDirOperator(QUrl(), m_opsWidget);
1064     m_ops->installEventFilter(q);
1065     m_ops->setObjectName(QStringLiteral("KFileWidget::ops"));
1066     m_ops->setIsSaving(m_operationMode == KFileWidget::Saving);
1067     m_ops->setNewFileMenuSelectDirWhenAlreadyExist(true);
1068     m_ops->showOpenWithActions(true);
1069     m_ops->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1070 
1071     auto bottomSparator = new QFrame(q);
1072     bottomSparator->setFrameStyle(QFrame::HLine);
1073 
1074     q->connect(m_ops, &KDirOperator::urlEntered, q, [this](const QUrl &url) {
1075         urlEntered(url);
1076     });
1077     q->connect(m_ops, &KDirOperator::fileHighlighted, q, [this](const KFileItem &item) {
1078         fileHighlighted(item);
1079     });
1080     q->connect(m_ops, &KDirOperator::fileSelected, q, [this](const KFileItem &item) {
1081         fileSelected(item);
1082     });
1083     q->connect(m_ops, &KDirOperator::finishedLoading, q, [this]() {
1084         slotLoadingFinished();
1085     });
1086     q->connect(m_ops, &KDirOperator::keyEnterReturnPressed, q, [this]() {
1087         slotViewKeyEnterReturnPressed();
1088     });
1089     q->connect(m_ops, &KDirOperator::renamingFinished, q, [this](const QList<QUrl> &urls) {
1090         // Update file names in location text field after renaming selected files
1091         q->setSelectedUrls(urls);
1092     });
1093 
1094     q->connect(m_ops, &KDirOperator::viewChanged, q, [](QAbstractItemView *newView) {
1095         newView->setProperty("_breeze_borders_sides", QVariant::fromValue(QFlags{Qt::TopEdge | Qt::BottomEdge}));
1096     });
1097 
1098     m_ops->dirLister()->setAutoErrorHandlingEnabled(false);
1099     q->connect(m_ops->dirLister(), &KDirLister::jobError, q, [this](KIO::Job *job) {
1100         m_messageWidget->setText(job->errorString());
1101         m_messageWidget->animatedShow();
1102     });
1103 
1104     m_ops->setupMenu(KDirOperator::SortActions | KDirOperator::FileActions | KDirOperator::ViewActions);
1105 
1106     initToolbar();
1107 
1108     m_opsWidgetLayout->addWidget(m_toolbar);
1109     m_opsWidgetLayout->addWidget(m_urlNavigator);
1110     m_opsWidgetLayout->addWidget(m_messageWidget);
1111     m_opsWidgetLayout->addWidget(topSeparator);
1112     m_opsWidgetLayout->addWidget(m_ops);
1113     m_opsWidgetLayout->addWidget(bottomSparator);
1114 }
1115 
1116 void KFileWidgetPrivate::initZoomWidget()
1117 {
1118     m_iconSizeSlider = new QSlider(q);
1119     m_iconSizeSlider->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
1120     m_iconSizeSlider->setMinimumWidth(40);
1121     m_iconSizeSlider->setOrientation(Qt::Horizontal);
1122     m_iconSizeSlider->setMinimum(0);
1123     m_iconSizeSlider->setMaximum(m_stdIconSizes.size() - 1);
1124     m_iconSizeSlider->setSingleStep(1);
1125     m_iconSizeSlider->setPageStep(1);
1126     m_iconSizeSlider->setTickPosition(QSlider::TicksBelow);
1127 
1128     q->connect(m_iconSizeSlider, &QAbstractSlider::valueChanged, q, [this](int step) {
1129         slotIconSizeChanged(m_stdIconSizes[step]);
1130     });
1131 
1132     q->connect(m_iconSizeSlider, &QAbstractSlider::sliderMoved, q, [this](int step) {
1133         slotIconSizeSliderMoved(m_stdIconSizes[step]);
1134     });
1135 
1136     q->connect(m_ops, &KDirOperator::currentIconSizeChanged, q, [this](int iconSize) {
1137         slotDirOpIconSizeChanged(iconSize);
1138     });
1139 
1140     m_zoomOutAction = KStandardAction::create(
1141         KStandardAction::ZoomOut,
1142         q,
1143         [this]() {
1144             changeIconsSize(ZoomOut);
1145         },
1146         q);
1147 
1148     q->addAction(m_zoomOutAction);
1149 
1150     m_zoomInAction = KStandardAction::create(
1151         KStandardAction::ZoomIn,
1152         q,
1153         [this]() {
1154             changeIconsSize(ZoomIn);
1155         },
1156         q);
1157 
1158     q->addAction(m_zoomInAction);
1159 }
1160 
1161 void KFileWidgetPrivate::initToolbar()
1162 {
1163     m_toolbar = new QToolBar(m_opsWidget);
1164     m_toolbar->setObjectName(QStringLiteral("KFileWidget::toolbar"));
1165     m_toolbar->setMovable(false);
1166 
1167     // add nav items to the toolbar
1168     //
1169     // NOTE:  The order of the button icons here differs from that
1170     // found in the file manager and web browser, but has been discussed
1171     // and agreed upon on the kde-core-devel mailing list:
1172     //
1173     // http://lists.kde.org/?l=kde-core-devel&m=116888382514090&w=2
1174 
1175     m_ops->action(KDirOperator::Up)
1176         ->setWhatsThis(i18n("<qt>Click this button to enter the parent folder.<br /><br />"
1177                             "For instance, if the current location is file:/home/konqi clicking this "
1178                             "button will take you to file:/home.</qt>"));
1179 
1180     m_ops->action(KDirOperator::Back)->setWhatsThis(i18n("Click this button to move backwards one step in the browsing history."));
1181     m_ops->action(KDirOperator::Forward)->setWhatsThis(i18n("Click this button to move forward one step in the browsing history."));
1182 
1183     m_ops->action(KDirOperator::Reload)->setWhatsThis(i18n("Click this button to reload the contents of the current location."));
1184     m_ops->action(KDirOperator::NewFolder)->setShortcuts(KStandardShortcut::createFolder());
1185     m_ops->action(KDirOperator::NewFolder)->setWhatsThis(i18n("Click this button to create a new folder."));
1186 
1187     m_togglePlacesPanelAction = new KToggleAction(i18n("Show Places Panel"), q);
1188     q->addAction(m_togglePlacesPanelAction);
1189     m_togglePlacesPanelAction->setShortcut(QKeySequence(Qt::Key_F9));
1190     q->connect(m_togglePlacesPanelAction, &QAction::toggled, q, [this](bool show) {
1191         togglePlacesPanel(show);
1192     });
1193 
1194     m_toggleBookmarksAction = new KToggleAction(i18n("Show Bookmarks Button"), q);
1195     q->addAction(m_toggleBookmarksAction);
1196     q->connect(m_toggleBookmarksAction, &QAction::toggled, q, [this](bool show) {
1197         toggleBookmarks(show);
1198     });
1199 
1200     // Build the settings menu
1201     KActionMenu *menu = new KActionMenu(QIcon::fromTheme(QStringLiteral("configure")), i18n("Options"), q);
1202     q->addAction(menu);
1203     menu->setWhatsThis(
1204         i18n("<qt>This is the preferences menu for the file dialog. "
1205              "Various options can be accessed from this menu including: <ul>"
1206              "<li>how files are sorted in the list</li>"
1207              "<li>types of view, including icon and list</li>"
1208              "<li>showing of hidden files</li>"
1209              "<li>the Places panel</li>"
1210              "<li>file previews</li>"
1211              "<li>separating folders from files</li></ul></qt>"));
1212 
1213     menu->addAction(m_ops->action(KDirOperator::AllowExpansionInDetailsView));
1214     menu->addSeparator();
1215     menu->addAction(m_ops->action(KDirOperator::ShowHiddenFiles));
1216     menu->addAction(m_togglePlacesPanelAction);
1217     menu->addAction(m_toggleBookmarksAction);
1218     menu->addAction(m_ops->action(KDirOperator::ShowPreviewPanel));
1219 
1220     menu->setPopupMode(QToolButton::InstantPopup);
1221     q->connect(menu->menu(), &QMenu::aboutToShow, m_ops, &KDirOperator::updateSelectionDependentActions);
1222 
1223     m_bookmarkButton = new KActionMenu(QIcon::fromTheme(QStringLiteral("bookmarks")), i18n("Bookmarks"), q);
1224     m_bookmarkButton->setPopupMode(QToolButton::InstantPopup);
1225     q->addAction(m_bookmarkButton);
1226     m_bookmarkButton->setWhatsThis(
1227         i18n("<qt>This button allows you to bookmark specific locations. "
1228              "Click on this button to open the bookmark menu where you may add, "
1229              "edit or select a bookmark.<br /><br />"
1230              "These bookmarks are specific to the file dialog, but otherwise operate "
1231              "like bookmarks elsewhere in KDE.</qt>"));
1232 
1233     QWidget *midSpacer = new QWidget(q);
1234     midSpacer->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1235 
1236     m_toolbar->addAction(m_ops->action(KDirOperator::Back));
1237     m_toolbar->addAction(m_ops->action(KDirOperator::Forward));
1238     m_toolbar->addAction(m_ops->action(KDirOperator::Up));
1239     m_toolbar->addAction(m_ops->action(KDirOperator::Reload));
1240     m_toolbar->addSeparator();
1241     m_toolbar->addAction(m_ops->action(KDirOperator::ViewIconsView));
1242     m_toolbar->addAction(m_ops->action(KDirOperator::ViewCompactView));
1243     m_toolbar->addAction(m_ops->action(KDirOperator::ViewDetailsView));
1244     m_toolbar->addSeparator();
1245     m_toolbar->addAction(m_ops->action(KDirOperator::ShowPreview));
1246     m_toolbar->addAction(m_ops->action(KDirOperator::SortMenu));
1247     m_toolbar->addAction(m_bookmarkButton);
1248 
1249     m_toolbar->addWidget(midSpacer);
1250 
1251     initZoomWidget();
1252     m_toolbar->addAction(m_zoomOutAction);
1253     m_toolbar->addWidget(m_iconSizeSlider);
1254     m_toolbar->addAction(m_zoomInAction);
1255     m_toolbar->addSeparator();
1256 
1257     m_toolbar->addAction(m_ops->action(KDirOperator::NewFolder));
1258     m_toolbar->addAction(menu);
1259 
1260     m_toolbar->setToolButtonStyle(Qt::ToolButtonIconOnly);
1261     m_toolbar->setMovable(false);
1262 }
1263 
1264 void KFileWidgetPrivate::initLocationWidget()
1265 {
1266     m_locationLabel = new QLabel(i18n("&Name:"), q);
1267     m_locationEdit = new KUrlComboBox(KUrlComboBox::Files, true, q);
1268     m_locationEdit->installEventFilter(q);
1269     // Properly let the dialog be resized (to smaller). Otherwise we could have
1270     // huge dialogs that can't be resized to smaller (it would be as big as the longest
1271     // item in this combo box). (ereslibre)
1272     m_locationEdit->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow);
1273     q->connect(m_locationEdit, &KUrlComboBox::editTextChanged, q, [this](const QString &text) {
1274         slotLocationChanged(text);
1275     });
1276 
1277     updateLocationWhatsThis();
1278     m_locationLabel->setBuddy(m_locationEdit);
1279 
1280     KUrlCompletion *fileCompletionObj = new KUrlCompletion(KUrlCompletion::FileCompletion);
1281     m_locationEdit->setCompletionObject(fileCompletionObj);
1282     m_locationEdit->setAutoDeleteCompletionObject(true);
1283 
1284     q->connect(m_locationEdit, &KUrlComboBox::returnPressed, q, [this](const QString &text) {
1285         locationAccepted(text);
1286     });
1287 }
1288 
1289 void KFileWidgetPrivate::initFilterWidget()
1290 {
1291     m_filterLabel = new QLabel(q);
1292     m_filterWidget = new KFileFilterCombo(q);
1293     m_filterWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
1294     updateFilterText();
1295     // Properly let the dialog be resized (to smaller). Otherwise we could have
1296     // huge dialogs that can't be resized to smaller (it would be as big as the longest
1297     // item in this combo box). (ereslibre)
1298     m_filterWidget->setSizeAdjustPolicy(QComboBox::AdjustToContentsOnFirstShow);
1299     m_filterLabel->setBuddy(m_filterWidget);
1300     q->connect(m_filterWidget, &KFileFilterCombo::filterChanged, q, [this]() {
1301         slotFilterChanged();
1302     });
1303 
1304     m_filterDelayTimer.setSingleShot(true);
1305     m_filterDelayTimer.setInterval(300);
1306     q->connect(m_filterWidget, &QComboBox::editTextChanged, &m_filterDelayTimer, qOverload<>(&QTimer::start));
1307     q->connect(&m_filterDelayTimer, &QTimer::timeout, q, [this]() {
1308         slotFilterChanged();
1309     });
1310 }
1311 
1312 void KFileWidgetPrivate::setLocationText(const QList<QUrl> &urlList)
1313 {
1314     // Block m_locationEdit signals as setCurrentItem() will cause textChanged() to get
1315     // emitted, so slotLocationChanged() will be called. Make sure we don't clear the
1316     // KDirOperator's view-selection in there
1317     const QSignalBlocker blocker(m_locationEdit);
1318 
1319     const QUrl baseUrl = m_ops->url();
1320 
1321     if (urlList.count() > 1) {
1322         QString urls;
1323         for (const QUrl &url : urlList) {
1324             urls += QStringLiteral("\"%1\" ").arg(escapeDoubleQuotes(relativePathOrUrl(baseUrl, url)));
1325         }
1326         urls.chop(1);
1327         m_locationEdit->setEditText(urls);
1328     } else if (urlList.count() == 1) {
1329         const auto url = urlList[0];
1330         m_locationEdit->setEditText(escapeDoubleQuotes(relativePathOrUrl(baseUrl, url)));
1331     } else if (!m_locationEdit->lineEdit()->text().isEmpty()) {
1332         m_locationEdit->clearEditText();
1333     }
1334 
1335     if (m_operationMode == KFileWidget::Saving) {
1336         setNonExtSelection();
1337     }
1338 }
1339 
1340 void KFileWidgetPrivate::updateLocationWhatsThis()
1341 {
1342     const QString autocompletionWhatsThisText = i18n(
1343         "<qt>While typing in the text area, you may be presented "
1344         "with possible matches. "
1345         "This feature can be controlled by clicking with the right mouse button "
1346         "and selecting a preferred mode from the <b>Text Completion</b> menu.</qt>");
1347 
1348     QString whatsThisText;
1349     if (m_operationMode == KFileWidget::Saving) {
1350         whatsThisText = QLatin1String("<qt>") + i18n("This is the name to save the file as.") + autocompletionWhatsThisText;
1351     } else if (m_ops->mode() & KFile::Files) {
1352         whatsThisText = QLatin1String("<qt>")
1353             + i18n("This is the list of files to open. More than "
1354                    "one file can be specified by listing several "
1355                    "files, separated by spaces.")
1356             + autocompletionWhatsThisText;
1357     } else {
1358         whatsThisText = QLatin1String("<qt>") + i18n("This is the name of the file to open.") + autocompletionWhatsThisText;
1359     }
1360 
1361     m_locationLabel->setWhatsThis(whatsThisText);
1362     m_locationEdit->setWhatsThis(whatsThisText);
1363 }
1364 
1365 void KFileWidgetPrivate::initPlacesPanel()
1366 {
1367     if (m_placesDock) {
1368         return;
1369     }
1370 
1371     m_placesDock = new QDockWidget(i18nc("@title:window", "Places"), q);
1372     m_placesDock->setFeatures(QDockWidget::NoDockWidgetFeatures);
1373     m_placesDock->setTitleBarWidget(new KDEPrivate::KFileWidgetDockTitleBar(m_placesDock));
1374 
1375     m_placesView = new KFilePlacesView(m_placesDock);
1376     m_placesView->setModel(m_model);
1377     m_placesView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
1378 
1379     m_placesView->setObjectName(QStringLiteral("url bar"));
1380     QObject::connect(m_placesView, &KFilePlacesView::urlChanged, q, [this](const QUrl &url) {
1381         enterUrl(url);
1382     });
1383 
1384     QObject::connect(qobject_cast<KFilePlacesModel *>(m_placesView->model()), &KFilePlacesModel::errorMessage, q, [this](const QString &errorMessage) {
1385         m_messageWidget->setText(errorMessage);
1386         m_messageWidget->animatedShow();
1387     });
1388 
1389     // need to set the current url of the urlbar manually (not via urlEntered()
1390     // here, because the initial url of KDirOperator might be the same as the
1391     // one that will be set later (and then urlEntered() won't be emitted).
1392     // TODO: KDE5 ### REMOVE THIS when KDirOperator's initial URL (in the c'tor) is gone.
1393     m_placesView->setUrl(m_url);
1394 
1395     m_placesDock->setWidget(m_placesView);
1396     m_placesViewSplitter->insertWidget(0, m_placesDock);
1397 
1398     // initialize the size of the splitter
1399     m_placesViewWidth = m_configGroup.readEntry(SpeedbarWidth, m_placesView->sizeHint().width());
1400 
1401     // Needed for when the dialog is shown with the places panel initially hidden
1402     setPlacesViewSplitterSizes();
1403 
1404     QObject::connect(m_placesDock, &QDockWidget::visibilityChanged, q, [this](bool visible) {
1405         togglePlacesPanel(visible, m_placesDock);
1406     });
1407 }
1408 
1409 void KFileWidgetPrivate::setPlacesViewSplitterSizes()
1410 {
1411     if (m_placesViewWidth > 0) {
1412         QList<int> sizes = m_placesViewSplitter->sizes();
1413         sizes[0] = m_placesViewWidth;
1414         sizes[1] = q->width() - m_placesViewWidth - m_placesViewSplitter->handleWidth();
1415         m_placesViewSplitter->setSizes(sizes);
1416     }
1417 }
1418 
1419 void KFileWidgetPrivate::initGUI()
1420 {
1421     delete m_boxLayout; // deletes all sub layouts
1422 
1423     m_boxLayout = new QVBoxLayout(q);
1424     m_boxLayout->setContentsMargins(0, 0, 0, 0); // no additional margin to the already existing
1425 
1426     m_placesViewSplitter = new QSplitter(q);
1427     m_placesViewSplitter->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
1428     m_placesViewSplitter->setChildrenCollapsible(false);
1429     m_boxLayout->addWidget(m_placesViewSplitter);
1430 
1431     QObject::connect(m_placesViewSplitter, &QSplitter::splitterMoved, q, [this](int pos, int index) {
1432         placesViewSplitterMoved(pos, index);
1433     });
1434     m_placesViewSplitter->insertWidget(0, m_opsWidget);
1435 
1436     m_lafBox = new QFormLayout();
1437     m_lafBox->setSpacing(q->style()->pixelMetric(QStyle::PM_LayoutVerticalSpacing));
1438     m_lafBox->setContentsMargins(q->style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
1439                                  q->style()->pixelMetric(QStyle::PM_LayoutTopMargin),
1440                                  q->style()->pixelMetric(QStyle::PM_LayoutRightMargin),
1441                                  0);
1442 
1443     m_lafBox->addRow(m_locationLabel, m_locationEdit);
1444     m_lafBox->addRow(m_filterLabel, m_filterWidget);
1445     // Add the "Automatically Select Extension" checkbox
1446     m_lafBox->addWidget(m_autoSelectExtCheckBox);
1447 
1448     m_opsWidgetLayout->addLayout(m_lafBox);
1449 
1450     auto hbox = new QHBoxLayout();
1451     hbox->setSpacing(q->style()->pixelMetric(QStyle::PM_LayoutHorizontalSpacing));
1452     hbox->setContentsMargins(q->style()->pixelMetric(QStyle::PM_LayoutTopMargin),
1453                              q->style()->pixelMetric(QStyle::PM_LayoutLeftMargin),
1454                              q->style()->pixelMetric(QStyle::PM_LayoutRightMargin),
1455                              q->style()->pixelMetric(QStyle::PM_LayoutBottomMargin));
1456 
1457     hbox->addStretch(2);
1458     hbox->addWidget(m_okButton);
1459     hbox->addWidget(m_cancelButton);
1460 
1461     m_opsWidgetLayout->addLayout(hbox);
1462 
1463     q->setTabOrder(m_ops, m_autoSelectExtCheckBox);
1464     q->setTabOrder(m_autoSelectExtCheckBox, m_locationEdit);
1465     q->setTabOrder(m_locationEdit, m_filterWidget);
1466     q->setTabOrder(m_filterWidget, m_okButton);
1467     q->setTabOrder(m_okButton, m_cancelButton);
1468     q->setTabOrder(m_cancelButton, m_urlNavigator);
1469     q->setTabOrder(m_urlNavigator, m_ops);
1470 }
1471 
1472 void KFileWidgetPrivate::slotFilterChanged()
1473 {
1474     m_filterDelayTimer.stop();
1475 
1476     KFileFilter filter = m_filterWidget->currentFilter();
1477 
1478     m_ops->clearFilter();
1479 
1480     if (!filter.mimePatterns().isEmpty()) {
1481         QStringList types = filter.mimePatterns();
1482         types.prepend(QStringLiteral("inode/directory"));
1483         m_ops->setMimeFilter(types);
1484     }
1485 
1486     const auto filePatterns = filter.filePatterns();
1487     const bool hasRegExSyntax = std::any_of(filePatterns.constBegin(), filePatterns.constEnd(), [](const QString &filter) {
1488         return filter.contains(QLatin1Char('*')) || filter.contains(QLatin1Char('?')) || filter.contains(QLatin1Char('['));
1489     });
1490 
1491     if (hasRegExSyntax) {
1492         m_ops->setNameFilter(filter.filePatterns().join(QLatin1Char(' ')));
1493     } else {
1494         m_ops->setNameFilter(QLatin1Char('*') + filePatterns.join(QLatin1Char('*')) + QLatin1Char('*'));
1495     }
1496 
1497     updateAutoSelectExtension();
1498 
1499     m_ops->updateDir();
1500 
1501     Q_EMIT q->filterChanged(filter);
1502 }
1503 
1504 void KFileWidget::setUrl(const QUrl &url, bool clearforward)
1505 {
1506     //     qDebug();
1507 
1508     d->m_ops->setUrl(url, clearforward);
1509 }
1510 
1511 // Protected
1512 void KFileWidgetPrivate::urlEntered(const QUrl &url)
1513 {
1514     //     qDebug();
1515 
1516     KUrlComboBox *pathCombo = m_urlNavigator->editor();
1517     if (pathCombo->count() != 0) { // little hack
1518         pathCombo->setUrl(url);
1519     }
1520 
1521     bool blocked = m_locationEdit->blockSignals(true);
1522     if (m_keepLocation) {
1523         const QUrl currentUrl = urlFromString(locationEditCurrentText());
1524         // iconNameForUrl will get the icon or fallback to a generic one
1525         m_locationEdit->setItemIcon(0, QIcon::fromTheme(KIO::iconNameForUrl(currentUrl)));
1526         // Preserve the text when clicking on the view (cf fileHighlighted)
1527         m_locationEdit->lineEdit()->setModified(true);
1528     }
1529 
1530     m_locationEdit->blockSignals(blocked);
1531 
1532     m_urlNavigator->setLocationUrl(url);
1533 
1534     // is triggered in ctor before completion object is set
1535     KUrlCompletion *completion = dynamic_cast<KUrlCompletion *>(m_locationEdit->completionObject());
1536     if (completion) {
1537         completion->setDir(url);
1538     }
1539 
1540     if (m_placesView) {
1541         m_placesView->setUrl(url);
1542     }
1543 
1544     m_messageWidget->hide();
1545 }
1546 
1547 void KFileWidgetPrivate::locationAccepted(const QString &url)
1548 {
1549     Q_UNUSED(url);
1550     //     qDebug();
1551     q->slotOk();
1552 }
1553 
1554 void KFileWidgetPrivate::enterUrl(const QUrl &url)
1555 {
1556     //     qDebug();
1557 
1558     // append '/' if needed: url combo does not add it
1559     // tokenize() expects it because it uses QUrl::adjusted(QUrl::RemoveFilename)
1560     QUrl u(url);
1561     Utils::appendSlashToPath(u);
1562     q->setUrl(u);
1563 
1564     // We need to check window()->focusWidget() instead of m_locationEdit->hasFocus
1565     // because when the window is showing up m_locationEdit
1566     // may still not have focus but it'll be the one that will have focus when the window
1567     // gets it and we don't want to steal its focus either
1568     if (q->window()->focusWidget() != m_locationEdit) {
1569         m_ops->setFocus();
1570     }
1571 }
1572 
1573 void KFileWidgetPrivate::enterUrl(const QString &url)
1574 {
1575     //     qDebug();
1576 
1577     enterUrl(urlFromString(KUrlCompletion::replacedPath(url, true, true)));
1578 }
1579 
1580 bool KFileWidgetPrivate::toOverwrite(const QUrl &url)
1581 {
1582     //     qDebug();
1583 
1584     KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo);
1585     KJobWidgets::setWindow(statJob, q);
1586     bool res = statJob->exec();
1587 
1588     if (res) {
1589         int ret = KMessageBox::warningContinueCancel(q,
1590                                                      i18n("The file \"%1\" already exists. Do you wish to overwrite it?", url.fileName()),
1591                                                      i18n("Overwrite File?"),
1592                                                      KStandardGuiItem::overwrite(),
1593                                                      KStandardGuiItem::cancel(),
1594                                                      QString(),
1595                                                      KMessageBox::Notify | KMessageBox::Dangerous);
1596 
1597         if (ret != KMessageBox::Continue) {
1598             m_locationEdit->setFocus();
1599             setNonExtSelection();
1600 
1601             return false;
1602         }
1603         return true;
1604     }
1605 
1606     return true;
1607 }
1608 
1609 void KFileWidget::setSelectedUrl(const QUrl &url)
1610 {
1611     // Honor protocols that do not support directory listing
1612     if (!url.isRelative() && !KProtocolManager::supportsListing(url)) {
1613         return;
1614     }
1615     d->setLocationText(url);
1616 }
1617 
1618 void KFileWidget::setSelectedUrls(const QList<QUrl> &urls)
1619 {
1620     if (urls.isEmpty()) {
1621         return;
1622     }
1623 
1624     // Honor protocols that do not support directory listing
1625     if (!urls[0].isRelative() && !KProtocolManager::supportsListing(urls[0])) {
1626         return;
1627     }
1628     d->setLocationText(urls);
1629 }
1630 
1631 void KFileWidgetPrivate::slotLoadingFinished()
1632 {
1633     const QString currentText = m_locationEdit->currentText();
1634     if (currentText.isEmpty()) {
1635         return;
1636     }
1637 
1638     m_ops->blockSignals(true);
1639     QUrl u(m_ops->url());
1640     if (currentText.startsWith(QLatin1Char('/'))) {
1641         u.setPath(currentText);
1642     } else {
1643         u.setPath(Utils::concatPaths(m_ops->url().path(), currentText));
1644     }
1645     m_ops->setCurrentItem(u);
1646     m_ops->blockSignals(false);
1647 }
1648 
1649 void KFileWidgetPrivate::slotLocationChanged(const QString &text)
1650 {
1651     //     qDebug();
1652 
1653     m_locationEdit->lineEdit()->setModified(true);
1654 
1655     if (text.isEmpty() && m_ops->view()) {
1656         m_ops->view()->clearSelection();
1657     }
1658 
1659     if (!m_locationEdit->lineEdit()->text().isEmpty()) {
1660         const QList<QUrl> urlList(tokenize(text));
1661         m_ops->setCurrentItems(urlList);
1662     }
1663 
1664     updateFilter();
1665 }
1666 
1667 QUrl KFileWidget::selectedUrl() const
1668 {
1669     //     qDebug();
1670 
1671     if (d->m_inAccept) {
1672         return d->m_url;
1673     } else {
1674         return QUrl();
1675     }
1676 }
1677 
1678 QList<QUrl> KFileWidget::selectedUrls() const
1679 {
1680     //     qDebug();
1681 
1682     QList<QUrl> list;
1683     if (d->m_inAccept) {
1684         if (d->m_ops->mode() & KFile::Files) {
1685             list = d->m_urlList;
1686         } else {
1687             list.append(d->m_url);
1688         }
1689     }
1690     return list;
1691 }
1692 
1693 QList<QUrl> KFileWidgetPrivate::tokenize(const QString &line) const
1694 {
1695     qCDebug(KIO_KFILEWIDGETS_FW) << "Tokenizing:" << line;
1696 
1697     QList<QUrl> urls;
1698     QUrl baseUrl(m_ops->url().adjusted(QUrl::RemoveFilename));
1699     Utils::appendSlashToPath(baseUrl);
1700 
1701     // A helper that creates, validates and appends a new url based
1702     // on the given filename.
1703     auto addUrl = [baseUrl, &urls](const QString &partial_name) {
1704         if (partial_name.trimmed().isEmpty()) {
1705             return;
1706         }
1707 
1708         // url could be absolute
1709         QUrl partial_url(partial_name);
1710         if (!partial_url.isValid()
1711             || partial_url.isRelative()
1712             // the text might look like a url scheme but not be a real one
1713             || (!partial_url.scheme().isEmpty() && (!partial_name.contains(QStringLiteral("://")) || !KProtocolInfo::isKnownProtocol(partial_url.scheme())))) {
1714             // We have to use setPath here, so that something like "test#file"
1715             // isn't interpreted to have path "test" and fragment "file".
1716             partial_url.clear();
1717             partial_url.setPath(partial_name);
1718         }
1719 
1720         // This returns QUrl(partial_name) for absolute URLs.
1721         // Otherwise, returns the concatenated url.
1722         if (partial_url.isRelative() || baseUrl.isParentOf(partial_url)) {
1723             partial_url = baseUrl.resolved(partial_url);
1724         }
1725 
1726         if (partial_url.isValid()) {
1727             urls.append(partial_url);
1728         } else {
1729             // This can happen in the first quote! (ex: ' "something here"')
1730             qCDebug(KIO_KFILEWIDGETS_FW) << "Discarding Invalid" << partial_url;
1731         }
1732     };
1733 
1734     // An iterative approach here where we toggle the "escape" flag
1735     // if we hit `\`. If we hit `"` and the escape flag is false,
1736     // we split
1737     QString partial_name;
1738     bool escape = false;
1739     for (int i = 0; i < line.length(); i++) {
1740         const QChar ch = line[i];
1741 
1742         // Handle any character previously escaped
1743         if (escape) {
1744             partial_name += ch;
1745             escape = false;
1746             continue;
1747         }
1748 
1749         // Handle escape start
1750         if (ch.toLatin1() == '\\') {
1751             escape = true;
1752             continue;
1753         }
1754 
1755         // Handle UNESCAPED quote (") since the above ifs are
1756         // dealing with the escaped ones
1757         if (ch.toLatin1() == '"') {
1758             addUrl(partial_name);
1759             partial_name.clear();
1760             continue;
1761         }
1762 
1763         // Any other character just append
1764         partial_name += ch;
1765     }
1766 
1767     // Handle the last item which is buffered in partial_name. This is
1768     // required for single-file selection dialogs since the name will not
1769     // be wrapped in quotes
1770     if (!partial_name.isEmpty()) {
1771         addUrl(partial_name);
1772         partial_name.clear();
1773     }
1774 
1775     return urls;
1776 }
1777 
1778 QString KFileWidget::selectedFile() const
1779 {
1780     //     qDebug();
1781 
1782     if (d->m_inAccept) {
1783         const QUrl url = d->mostLocalUrl(d->m_url);
1784         if (url.isLocalFile()) {
1785             return url.toLocalFile();
1786         } else {
1787             KMessageBox::error(const_cast<KFileWidget *>(this), i18n("You can only select local files."), i18n("Remote Files Not Accepted"));
1788         }
1789     }
1790     return QString();
1791 }
1792 
1793 QStringList KFileWidget::selectedFiles() const
1794 {
1795     //     qDebug();
1796 
1797     QStringList list;
1798 
1799     if (d->m_inAccept) {
1800         if (d->m_ops->mode() & KFile::Files) {
1801             const QList<QUrl> urls = d->m_urlList;
1802             for (const auto &u : urls) {
1803                 const QUrl url = d->mostLocalUrl(u);
1804                 if (url.isLocalFile()) {
1805                     list.append(url.toLocalFile());
1806                 }
1807             }
1808         }
1809 
1810         else { // single-selection mode
1811             if (d->m_url.isLocalFile()) {
1812                 list.append(d->m_url.toLocalFile());
1813             }
1814         }
1815     }
1816 
1817     return list;
1818 }
1819 
1820 QUrl KFileWidget::baseUrl() const
1821 {
1822     return d->m_ops->url();
1823 }
1824 
1825 void KFileWidget::resizeEvent(QResizeEvent *event)
1826 {
1827     QWidget::resizeEvent(event);
1828 
1829     if (d->m_placesDock) {
1830         // we don't want our places dock actually changing size when we resize
1831         // and qt doesn't make it easy to enforce such a thing with QSplitter
1832         d->setPlacesViewSplitterSizes();
1833     }
1834 }
1835 
1836 void KFileWidget::showEvent(QShowEvent *event)
1837 {
1838     if (!d->m_hasView) { // delayed view-creation
1839         Q_ASSERT(d);
1840         Q_ASSERT(d->m_ops);
1841         d->m_ops->setViewMode(KFile::Default);
1842         d->m_hasView = true;
1843 
1844         connect(d->m_ops->view(), &QAbstractItemView::doubleClicked, this, [this](const QModelIndex &index) {
1845             d->slotViewDoubleClicked(index);
1846         });
1847     }
1848     d->m_ops->clearHistory();
1849 
1850     QWidget::showEvent(event);
1851 }
1852 
1853 bool KFileWidget::eventFilter(QObject *watched, QEvent *event)
1854 {
1855     const bool res = QWidget::eventFilter(watched, event);
1856 
1857     QKeyEvent *keyEvent = dynamic_cast<QKeyEvent *>(event);
1858     if (!keyEvent) {
1859         return res;
1860     }
1861 
1862     const auto type = event->type();
1863     const auto key = keyEvent->key();
1864 
1865     if (watched == d->m_ops && type == QEvent::KeyPress && (key == Qt::Key_Return || key == Qt::Key_Enter)) {
1866         // ignore return events from the KDirOperator
1867         // they are not needed, activated is used to handle this case
1868         event->accept();
1869         return true;
1870     }
1871 
1872     return res;
1873 }
1874 
1875 void KFileWidget::setMode(KFile::Modes m)
1876 {
1877     //     qDebug();
1878 
1879     d->m_ops->setMode(m);
1880     if (d->m_ops->dirOnlyMode()) {
1881         d->m_filterWidget->setDefaultFilter(KFileFilter(i18n("All Folders"), {QStringLiteral("*")}, {}));
1882     } else {
1883         d->m_filterWidget->setDefaultFilter(KFileFilter(i18n("All Files"), {QStringLiteral("*")}, {}));
1884     }
1885 
1886     d->updateAutoSelectExtension();
1887 }
1888 
1889 KFile::Modes KFileWidget::mode() const
1890 {
1891     return d->m_ops->mode();
1892 }
1893 
1894 void KFileWidgetPrivate::readViewConfig()
1895 {
1896     m_ops->setViewConfig(m_configGroup);
1897     m_ops->readConfig(m_configGroup);
1898     KUrlComboBox *combo = m_urlNavigator->editor();
1899 
1900     KCompletion::CompletionMode cm =
1901         (KCompletion::CompletionMode)m_configGroup.readEntry(PathComboCompletionMode, static_cast<int>(KCompletion::CompletionPopup));
1902     if (cm != KCompletion::CompletionPopup) {
1903         combo->setCompletionMode(cm);
1904     }
1905 
1906     cm = (KCompletion::CompletionMode)m_configGroup.readEntry(LocationComboCompletionMode, static_cast<int>(KCompletion::CompletionPopup));
1907     if (cm != KCompletion::CompletionPopup) {
1908         m_locationEdit->setCompletionMode(cm);
1909     }
1910 
1911     // Show or don't show the places panel
1912     togglePlacesPanel(m_configGroup.readEntry(ShowSpeedbar, true));
1913 
1914     // show or don't show the bookmarks
1915     toggleBookmarks(m_configGroup.readEntry(ShowBookmarks, false));
1916 
1917     // does the user want Automatically Select Extension?
1918     m_autoSelectExtChecked = m_configGroup.readEntry(AutoSelectExtChecked, DefaultAutoSelectExtChecked);
1919     updateAutoSelectExtension();
1920 
1921     // should the URL navigator use the breadcrumb navigation?
1922     m_urlNavigator->setUrlEditable(!m_configGroup.readEntry(BreadcrumbNavigation, true));
1923 
1924     // should the URL navigator show the full path?
1925     m_urlNavigator->setShowFullPath(m_configGroup.readEntry(ShowFullPath, false));
1926 
1927     int w1 = q->minimumSize().width();
1928     int w2 = m_toolbar->sizeHint().width();
1929     if (w1 < w2) {
1930         q->setMinimumWidth(w2);
1931     }
1932 }
1933 
1934 void KFileWidgetPrivate::writeViewConfig()
1935 {
1936     // these settings are global settings; ALL instances of the file dialog
1937     // should reflect them.
1938     // There is no way to tell KFileOperator::writeConfig() to write to
1939     // kdeglobals so we write settings to a temporary config group then copy
1940     // them all to kdeglobals
1941     KConfig tmp(QString(), KConfig::SimpleConfig);
1942     KConfigGroup tmpGroup(&tmp, ConfigGroup);
1943 
1944     KUrlComboBox *pathCombo = m_urlNavigator->editor();
1945     // saveDialogSize( tmpGroup, KConfigGroup::Persistent | KConfigGroup::Global );
1946     tmpGroup.writeEntry(PathComboCompletionMode, static_cast<int>(pathCombo->completionMode()));
1947     tmpGroup.writeEntry(LocationComboCompletionMode, static_cast<int>(m_locationEdit->completionMode()));
1948 
1949     const bool showPlacesPanel = m_placesDock && !m_placesDock->isHidden();
1950     tmpGroup.writeEntry(ShowSpeedbar, showPlacesPanel);
1951     if (m_placesViewWidth > 0) {
1952         tmpGroup.writeEntry(SpeedbarWidth, m_placesViewWidth);
1953     }
1954 
1955     tmpGroup.writeEntry(ShowBookmarks, m_bookmarkHandler != nullptr);
1956     tmpGroup.writeEntry(AutoSelectExtChecked, m_autoSelectExtChecked);
1957     tmpGroup.writeEntry(BreadcrumbNavigation, !m_urlNavigator->isUrlEditable());
1958     tmpGroup.writeEntry(ShowFullPath, m_urlNavigator->showFullPath());
1959 
1960     m_ops->writeConfig(tmpGroup);
1961 
1962     // Copy saved settings to kdeglobals
1963     tmpGroup.copyTo(&m_configGroup, KConfigGroup::Persistent | KConfigGroup::Global);
1964 }
1965 
1966 void KFileWidgetPrivate::readRecentFiles()
1967 {
1968     //     qDebug();
1969 
1970     const bool oldState = m_locationEdit->blockSignals(true);
1971     m_locationEdit->setMaxItems(m_configGroup.readEntry(RecentFilesNumber, DefaultRecentURLsNumber));
1972     m_locationEdit->setUrls(m_configGroup.readPathEntry(RecentFiles, QStringList()), KUrlComboBox::RemoveBottom);
1973     m_locationEdit->setCurrentIndex(-1);
1974     m_locationEdit->blockSignals(oldState);
1975 
1976     KUrlComboBox *combo = m_urlNavigator->editor();
1977     combo->setUrls(m_configGroup.readPathEntry(RecentURLs, QStringList()), KUrlComboBox::RemoveTop);
1978     combo->setMaxItems(m_configGroup.readEntry(RecentURLsNumber, DefaultRecentURLsNumber));
1979     combo->setUrl(m_ops->url());
1980     // since we delayed this moment, initialize the directory of the completion object to
1981     // our current directory (that was very probably set on the constructor)
1982     KUrlCompletion *completion = dynamic_cast<KUrlCompletion *>(m_locationEdit->completionObject());
1983     if (completion) {
1984         completion->setDir(m_ops->url());
1985     }
1986 }
1987 
1988 void KFileWidgetPrivate::saveRecentFiles()
1989 {
1990     //     qDebug();
1991     m_configGroup.writePathEntry(RecentFiles, m_locationEdit->urls());
1992 
1993     KUrlComboBox *pathCombo = m_urlNavigator->editor();
1994     m_configGroup.writePathEntry(RecentURLs, pathCombo->urls());
1995 }
1996 
1997 QPushButton *KFileWidget::okButton() const
1998 {
1999     return d->m_okButton;
2000 }
2001 
2002 QPushButton *KFileWidget::cancelButton() const
2003 {
2004     return d->m_cancelButton;
2005 }
2006 
2007 // Called by KFileDialog
2008 void KFileWidget::slotCancel()
2009 {
2010     d->writeViewConfig();
2011     d->m_ops->close();
2012 }
2013 
2014 void KFileWidget::setKeepLocation(bool keep)
2015 {
2016     d->m_keepLocation = keep;
2017 }
2018 
2019 bool KFileWidget::keepsLocation() const
2020 {
2021     return d->m_keepLocation;
2022 }
2023 
2024 void KFileWidget::setOperationMode(OperationMode mode)
2025 {
2026     //     qDebug();
2027 
2028     d->m_operationMode = mode;
2029     d->m_keepLocation = (mode == Saving);
2030     d->m_filterWidget->setEditable(!d->m_hasDefaultFilter || mode != Saving);
2031     if (mode == Opening) {
2032         // don't use KStandardGuiItem::open() here which has trailing ellipsis!
2033         d->m_okButton->setText(i18n("&Open"));
2034         d->m_okButton->setIcon(QIcon::fromTheme(QStringLiteral("document-open")));
2035         // hide the new folder actions...usability team says they shouldn't be in open file dialog
2036         d->m_ops->action(KDirOperator::NewFolder)->setEnabled(false);
2037         d->m_toolbar->removeAction(d->m_ops->action(KDirOperator::NewFolder));
2038     } else if (mode == Saving) {
2039         KGuiItem::assign(d->m_okButton, KStandardGuiItem::save());
2040         d->setNonExtSelection();
2041     } else {
2042         KGuiItem::assign(d->m_okButton, KStandardGuiItem::ok());
2043     }
2044     d->updateLocationWhatsThis();
2045     d->updateAutoSelectExtension();
2046 
2047     if (d->m_ops) {
2048         d->m_ops->setIsSaving(mode == Saving);
2049     }
2050     d->updateFilterText();
2051 }
2052 
2053 KFileWidget::OperationMode KFileWidget::operationMode() const
2054 {
2055     return d->m_operationMode;
2056 }
2057 
2058 void KFileWidgetPrivate::slotAutoSelectExtClicked()
2059 {
2060     //     qDebug() << "slotAutoSelectExtClicked(): "
2061     //                          << m_autoSelectExtCheckBox->isChecked() << endl;
2062 
2063     // whether the _user_ wants it on/off
2064     m_autoSelectExtChecked = m_autoSelectExtCheckBox->isChecked();
2065 
2066     // update the current filename's extension
2067     updateLocationEditExtension(m_extension /* extension hasn't changed */);
2068 }
2069 
2070 void KFileWidgetPrivate::placesViewSplitterMoved(int pos, int index)
2071 {
2072     //     qDebug();
2073 
2074     // we need to record the size of the splitter when the splitter changes size
2075     // so we can keep the places box the right size!
2076     if (m_placesDock && index == 1) {
2077         m_placesViewWidth = pos;
2078         //         qDebug() << "setting m_lafBox minwidth to" << m_placesViewWidth;
2079     }
2080 }
2081 
2082 void KFileWidgetPrivate::activateUrlNavigator()
2083 {
2084     //     qDebug();
2085 
2086     QLineEdit *lineEdit = m_urlNavigator->editor()->lineEdit();
2087 
2088     // If the text field currently has focus and everything is selected,
2089     // pressing the keyboard shortcut returns the whole thing to breadcrumb mode
2090     if (m_urlNavigator->isUrlEditable() && lineEdit->hasFocus() && lineEdit->selectedText() == lineEdit->text()) {
2091         m_urlNavigator->setUrlEditable(false);
2092     } else {
2093         m_urlNavigator->setUrlEditable(true);
2094         m_urlNavigator->setFocus();
2095         lineEdit->selectAll();
2096     }
2097 }
2098 
2099 void KFileWidgetPrivate::slotDirOpIconSizeChanged(int size)
2100 {
2101     auto beginIt = m_stdIconSizes.cbegin();
2102     auto endIt = m_stdIconSizes.cend();
2103     auto it = std::lower_bound(beginIt, endIt, size);
2104     const int sliderStep = it != endIt ? it - beginIt : 0;
2105     m_iconSizeSlider->setValue(sliderStep);
2106     m_zoomOutAction->setDisabled(it == beginIt);
2107     m_zoomInAction->setDisabled(it == (endIt - 1));
2108 }
2109 
2110 void KFileWidgetPrivate::changeIconsSize(ZoomState zoom)
2111 {
2112     int step = m_iconSizeSlider->value();
2113 
2114     if (zoom == ZoomOut) {
2115         if (step == 0) {
2116             return;
2117         }
2118         --step;
2119     } else { // ZoomIn
2120         if (step == static_cast<int>(m_stdIconSizes.size() - 1)) {
2121             return;
2122         }
2123         ++step;
2124     }
2125 
2126     m_iconSizeSlider->setValue(step);
2127     slotIconSizeSliderMoved(m_stdIconSizes[step]);
2128 }
2129 
2130 void KFileWidgetPrivate::slotIconSizeChanged(int _value)
2131 {
2132     m_ops->setIconSize(_value);
2133     m_iconSizeSlider->setToolTip(i18n("Icon size: %1 pixels", _value));
2134 }
2135 
2136 void KFileWidgetPrivate::slotIconSizeSliderMoved(int size)
2137 {
2138     // Force this to be called in case this slot is called first on the
2139     // slider move.
2140     slotIconSizeChanged(size);
2141 
2142     QPoint global(m_iconSizeSlider->rect().topLeft());
2143     global.ry() += m_iconSizeSlider->height() / 2;
2144     QHelpEvent toolTipEvent(QEvent::ToolTip, QPoint(0, 0), m_iconSizeSlider->mapToGlobal(global));
2145     QApplication::sendEvent(m_iconSizeSlider, &toolTipEvent);
2146 }
2147 
2148 void KFileWidgetPrivate::slotViewDoubleClicked(const QModelIndex &index)
2149 {
2150     // double clicking to save should only work on files
2151     if (m_operationMode == KFileWidget::Saving && index.isValid() && m_ops->selectedItems().constFirst().isFile()) {
2152         q->slotOk();
2153     }
2154 }
2155 
2156 void KFileWidgetPrivate::slotViewKeyEnterReturnPressed()
2157 {
2158     // an enter/return event occurred in the view
2159     // when we are saving one file and there is no selection in the view (otherwise we get an activated event)
2160     if (m_operationMode == KFileWidget::Saving && (m_ops->mode() & KFile::File) && m_ops->selectedItems().isEmpty()) {
2161         q->slotOk();
2162     }
2163 }
2164 
2165 static QString getExtensionFromPatternList(const QStringList &patternList)
2166 {
2167     //     qDebug();
2168 
2169     QString ret;
2170     //     qDebug() << "\tgetExtension " << patternList;
2171 
2172     QStringList::ConstIterator patternListEnd = patternList.end();
2173     for (QStringList::ConstIterator it = patternList.begin(); it != patternListEnd; ++it) {
2174         //         qDebug() << "\t\ttry: \'" << (*it) << "\'";
2175 
2176         // is this pattern like "*.BMP" rather than useless things like:
2177         //
2178         // README
2179         // *.
2180         // *.*
2181         // *.JP*G
2182         // *.JP?
2183         // *.[Jj][Pp][Gg]
2184         if ((*it).startsWith(QLatin1String("*.")) && (*it).length() > 2 && (*it).indexOf(QLatin1Char('*'), 2) < 0 && (*it).indexOf(QLatin1Char('?'), 2) < 0
2185             && (*it).indexOf(QLatin1Char('['), 2) < 0 && (*it).indexOf(QLatin1Char(']'), 2) < 0) {
2186             ret = (*it).mid(1);
2187             break;
2188         }
2189     }
2190 
2191     return ret;
2192 }
2193 
2194 static QString stripUndisplayable(const QString &string)
2195 {
2196     QString ret = string;
2197 
2198     ret.remove(QLatin1Char(':'));
2199     ret = KLocalizedString::removeAcceleratorMarker(ret);
2200 
2201     return ret;
2202 }
2203 
2204 // QString KFileWidget::currentFilterExtension()
2205 //{
2206 //    return d->m_extension;
2207 //}
2208 
2209 void KFileWidgetPrivate::updateAutoSelectExtension()
2210 {
2211     if (!m_autoSelectExtCheckBox) {
2212         return;
2213     }
2214 
2215     QMimeDatabase db;
2216     //
2217     // Figure out an extension for the Automatically Select Extension thing
2218     // (some Windows users apparently don't know what to do when confronted
2219     // with a text file called "COPYING" but do know what to do with
2220     // COPYING.txt ...)
2221     //
2222 
2223     //     qDebug() << "Figure out an extension: ";
2224     QString lastExtension = m_extension;
2225     m_extension.clear();
2226 
2227     // Automatically Select Extension is only valid if the user is _saving_ a _file_
2228     if ((m_operationMode == KFileWidget::Saving) && (m_ops->mode() & KFile::File)) {
2229         //
2230         // Get an extension from the filter
2231         //
2232 
2233         KFileFilter fileFilter = m_filterWidget->currentFilter();
2234         if (!fileFilter.isEmpty()) {
2235             // if the currently selected filename already has an extension which
2236             // is also included in the currently allowed extensions, keep it
2237             // otherwise use the default extension
2238             QString currentExtension = db.suffixForFileName(locationEditCurrentText());
2239             if (currentExtension.isEmpty()) {
2240                 currentExtension = locationEditCurrentText().section(QLatin1Char('.'), -1, -1);
2241             }
2242             // qDebug() << "filter:" << filter << "locationEdit:" << locationEditCurrentText() << "currentExtension:" << currentExtension;
2243 
2244             QString defaultExtension;
2245             QStringList extensionList;
2246 
2247             // e.g. "*.cpp"
2248             if (!fileFilter.filePatterns().isEmpty()) {
2249                 extensionList = fileFilter.filePatterns();
2250                 defaultExtension = getExtensionFromPatternList(extensionList);
2251             }
2252             // e.g. "text/html"
2253             else if (!fileFilter.mimePatterns().isEmpty()) {
2254                 QMimeType mime = db.mimeTypeForName(fileFilter.mimePatterns().first());
2255                 if (mime.isValid()) {
2256                     extensionList = mime.globPatterns();
2257                     defaultExtension = mime.preferredSuffix();
2258                     if (!defaultExtension.isEmpty()) {
2259                         defaultExtension.prepend(QLatin1Char('.'));
2260                     }
2261                 }
2262             }
2263 
2264             if ((!currentExtension.isEmpty() && extensionList.contains(QLatin1String("*.") + currentExtension))
2265                 || (!fileFilter.mimePatterns().isEmpty() && fileFilter.mimePatterns().first() == QLatin1String("application/octet-stream"))) {
2266                 m_extension = QLatin1Char('.') + currentExtension;
2267             } else {
2268                 m_extension = defaultExtension;
2269             }
2270 
2271             // qDebug() << "List:" << extensionList << "auto-selected extension:" << m_extension;
2272         }
2273 
2274         //
2275         // GUI: checkbox
2276         //
2277 
2278         QString whatsThisExtension;
2279         if (!m_extension.isEmpty()) {
2280             // remember: sync any changes to the string with below
2281             m_autoSelectExtCheckBox->setText(i18n("Automatically select filename e&xtension (%1)", m_extension));
2282             whatsThisExtension = i18n("the extension <b>%1</b>", m_extension);
2283 
2284             m_autoSelectExtCheckBox->setEnabled(true);
2285             m_autoSelectExtCheckBox->setChecked(m_autoSelectExtChecked);
2286         } else {
2287             // remember: sync any changes to the string with above
2288             m_autoSelectExtCheckBox->setText(i18n("Automatically select filename e&xtension"));
2289             whatsThisExtension = i18n("a suitable extension");
2290 
2291             m_autoSelectExtCheckBox->setChecked(false);
2292             m_autoSelectExtCheckBox->setEnabled(false);
2293         }
2294 
2295         const QString locationLabelText = stripUndisplayable(m_locationLabel->text());
2296         m_autoSelectExtCheckBox->setWhatsThis(QLatin1String("<qt>")
2297                                               + i18n("This option enables some convenient features for "
2298                                                      "saving files with extensions:<br />"
2299                                                      "<ol>"
2300                                                      "<li>Any extension specified in the <b>%1</b> text "
2301                                                      "area will be updated if you change the file type "
2302                                                      "to save in.<br />"
2303                                                      "<br /></li>"
2304                                                      "<li>If no extension is specified in the <b>%2</b> "
2305                                                      "text area when you click "
2306                                                      "<b>Save</b>, %3 will be added to the end of the "
2307                                                      "filename (if the filename does not already exist). "
2308                                                      "This extension is based on the file type that you "
2309                                                      "have chosen to save in.<br />"
2310                                                      "<br />"
2311                                                      "If you do not want KDE to supply an extension for the "
2312                                                      "filename, you can either turn this option off or you "
2313                                                      "can suppress it by adding a period (.) to the end of "
2314                                                      "the filename (the period will be automatically "
2315                                                      "removed)."
2316                                                      "</li>"
2317                                                      "</ol>"
2318                                                      "If unsure, keep this option enabled as it makes your "
2319                                                      "files more manageable.",
2320                                                      locationLabelText,
2321                                                      locationLabelText,
2322                                                      whatsThisExtension)
2323                                               + QLatin1String("</qt>"));
2324 
2325         m_autoSelectExtCheckBox->show();
2326 
2327         // update the current filename's extension
2328         updateLocationEditExtension(lastExtension);
2329     }
2330     // Automatically Select Extension not valid
2331     else {
2332         m_autoSelectExtCheckBox->setChecked(false);
2333         m_autoSelectExtCheckBox->hide();
2334     }
2335 }
2336 
2337 // Updates the extension of the filename specified in d->m_locationEdit if the
2338 // Automatically Select Extension feature is enabled.
2339 // (this prevents you from accidentally saving "file.kwd" as RTF, for example)
2340 void KFileWidgetPrivate::updateLocationEditExtension(const QString &lastExtension)
2341 {
2342     if (!m_autoSelectExtCheckBox->isChecked() || m_extension.isEmpty()) {
2343         return;
2344     }
2345 
2346     const QString urlStr = locationEditCurrentText();
2347     if (urlStr.isEmpty()) {
2348         return;
2349     }
2350 
2351     const int fileNameOffset = urlStr.lastIndexOf(QLatin1Char('/')) + 1;
2352     QStringView fileName = QStringView(urlStr).mid(fileNameOffset);
2353 
2354     const int dot = fileName.lastIndexOf(QLatin1Char('.'));
2355     const int len = fileName.length();
2356     if (dot > 0 && // has an extension already and it's not a hidden file
2357                    // like ".hidden" (but we do accept ".hidden.ext")
2358         dot != len - 1 // and not deliberately suppressing extension
2359     ) {
2360         const QUrl url = getCompleteUrl(urlStr);
2361         //     qDebug() << "updateLocationEditExtension (" << url << ")";
2362         // exists?
2363         KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo);
2364         KJobWidgets::setWindow(statJob, q);
2365         bool result = statJob->exec();
2366         if (result) {
2367             //             qDebug() << "\tfile exists";
2368 
2369             if (statJob->statResult().isDir()) {
2370                 //                 qDebug() << "\tisDir - won't alter extension";
2371                 return;
2372             }
2373 
2374             // --- fall through ---
2375         }
2376 
2377         //
2378         // try to get rid of the current extension
2379         //
2380 
2381         // catch "double extensions" like ".tar.gz"
2382         if (!lastExtension.isEmpty() && fileName.endsWith(lastExtension)) {
2383             fileName.chop(lastExtension.length());
2384         } else if (!m_extension.isEmpty() && fileName.endsWith(m_extension)) {
2385             fileName.chop(m_extension.length());
2386         } else { // can only handle "single extensions"
2387             fileName.truncate(dot);
2388         }
2389 
2390         // add extension
2391         const QString newText = QStringView(urlStr).left(fileNameOffset) + fileName + m_extension;
2392         if (newText != locationEditCurrentText()) {
2393             const int idx = m_locationEdit->currentIndex();
2394             if (idx == -1) {
2395                 m_locationEdit->setEditText(newText);
2396             } else {
2397                 m_locationEdit->setItemText(idx, newText);
2398             }
2399             m_locationEdit->lineEdit()->setModified(true);
2400         }
2401     }
2402 }
2403 
2404 QString KFileWidgetPrivate::findMatchingFilter(const QString &filter, const QString &filename) const
2405 {
2406     // e.g.: '*.foo *.bar|Foo type' -> '*.foo', '*.bar'
2407     const QStringList patterns = filter.left(filter.indexOf(QLatin1Char('|'))).split(QLatin1Char(' '), Qt::SkipEmptyParts);
2408 
2409     QRegularExpression rx;
2410     for (const QString &p : patterns) {
2411         rx.setPattern(QRegularExpression::wildcardToRegularExpression(p));
2412         if (rx.match(filename).hasMatch()) {
2413             return p;
2414         }
2415     }
2416     return QString();
2417 }
2418 
2419 // Updates the filter if the extension of the filename specified in d->m_locationEdit is changed
2420 // (this prevents you from accidentally saving "file.kwd" as RTF, for example)
2421 void KFileWidgetPrivate::updateFilter()
2422 {
2423     if ((m_operationMode == KFileWidget::Saving) && (m_ops->mode() & KFile::File)) {
2424         QString urlStr = locationEditCurrentText();
2425         if (urlStr.isEmpty()) {
2426             return;
2427         }
2428 
2429         QMimeDatabase db;
2430         QMimeType urlMimeType = db.mimeTypeForFile(urlStr, QMimeDatabase::MatchExtension);
2431 
2432         bool matchesCurrentFilter = [this, urlMimeType, urlStr] {
2433             const KFileFilter filter = m_filterWidget->currentFilter();
2434             if (filter.mimePatterns().contains(urlMimeType.name())) {
2435                 return true;
2436             }
2437 
2438             QString filename = urlStr.mid(urlStr.lastIndexOf(QLatin1Char('/')) + 1); // only filename
2439 
2440             const auto filePatterns = filter.filePatterns();
2441             const bool hasMatch = std::any_of(filePatterns.cbegin(), filePatterns.cend(), [filename](const QString &pattern) {
2442                 QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(pattern));
2443 
2444                 return rx.match(filename).hasMatch();
2445             });
2446             return hasMatch;
2447         }();
2448 
2449         if (matchesCurrentFilter) {
2450             return;
2451         }
2452 
2453         const auto filters = m_filterWidget->filters();
2454 
2455         auto filterIt = std::find_if(filters.cbegin(), filters.cend(), [urlStr, urlMimeType](const KFileFilter &filter) {
2456             if (filter.mimePatterns().contains(urlMimeType.name())) {
2457                 return true;
2458             }
2459 
2460             QString filename = urlStr.mid(urlStr.lastIndexOf(QLatin1Char('/')) + 1); // only filename
2461             // accept any match to honor the user's selection; see later code handling the "*" match
2462 
2463             const auto filePatterns = filter.filePatterns();
2464             const bool hasMatch = std::any_of(filePatterns.cbegin(), filePatterns.cend(), [filename](const QString &pattern) {
2465                 // never match the catch-all filter
2466                 if (pattern == QLatin1String("*")) {
2467                     return false;
2468                 }
2469 
2470                 QRegularExpression rx(QRegularExpression::wildcardToRegularExpression(pattern));
2471 
2472                 return rx.match(filename).hasMatch();
2473             });
2474 
2475             return hasMatch;
2476         });
2477 
2478         if (filterIt != filters.cend()) {
2479             m_filterWidget->setCurrentFilter(*filterIt);
2480         }
2481     }
2482 }
2483 
2484 // applies only to a file that doesn't already exist
2485 void KFileWidgetPrivate::appendExtension(QUrl &url)
2486 {
2487     //     qDebug();
2488 
2489     if (!m_autoSelectExtCheckBox->isChecked() || m_extension.isEmpty()) {
2490         return;
2491     }
2492 
2493     QString fileName = url.fileName();
2494     if (fileName.isEmpty()) {
2495         return;
2496     }
2497 
2498     //     qDebug() << "appendExtension(" << url << ")";
2499 
2500     const int len = fileName.length();
2501     const int dot = fileName.lastIndexOf(QLatin1Char('.'));
2502 
2503     const bool suppressExtension = (dot == len - 1);
2504     const bool unspecifiedExtension = !fileName.endsWith(m_extension);
2505 
2506     // don't KIO::Stat if unnecessary
2507     if (!(suppressExtension || unspecifiedExtension)) {
2508         return;
2509     }
2510 
2511     // exists?
2512     KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo);
2513     KJobWidgets::setWindow(statJob, q);
2514     bool res = statJob->exec();
2515     if (res) {
2516         //         qDebug() << "\tfile exists - won't append extension";
2517         return;
2518     }
2519 
2520     // suppress automatically append extension?
2521     if (suppressExtension) {
2522         //
2523         // Strip trailing dot
2524         // This allows lazy people to have m_autoSelectExtCheckBox->isChecked
2525         // but don't want a file extension to be appended
2526         // e.g. "README." will make a file called "README"
2527         //
2528         // If you really want a name like "README.", then type "README.."
2529         // and the trailing dot will be removed (or just stop being lazy and
2530         // turn off this feature so that you can type "README.")
2531         //
2532         //         qDebug() << "\tstrip trailing dot";
2533         QString path = url.path();
2534         path.chop(1);
2535         url.setPath(path);
2536     }
2537     // evilmatically append extension :) if the user hasn't specified one
2538     else if (unspecifiedExtension) {
2539         //         qDebug() << "\tappending extension \'" << m_extension << "\'...";
2540         url = url.adjusted(QUrl::RemoveFilename); // keeps trailing slash
2541         url.setPath(url.path() + fileName + m_extension);
2542         //         qDebug() << "\tsaving as \'" << url << "\'";
2543     }
2544 }
2545 
2546 // adds the selected files/urls to 'recent documents'
2547 void KFileWidgetPrivate::addToRecentDocuments()
2548 {
2549     int m = m_ops->mode();
2550     int atmost = KRecentDocument::maximumItems();
2551     // don't add more than we need. KRecentDocument::add() is pretty slow
2552 
2553     if (m & KFile::LocalOnly) {
2554         const QStringList files = q->selectedFiles();
2555         QStringList::ConstIterator it = files.begin();
2556         for (; it != files.end() && atmost > 0; ++it) {
2557             KRecentDocument::add(QUrl::fromLocalFile(*it));
2558             atmost--;
2559         }
2560     }
2561 
2562     else { // urls
2563         const QList<QUrl> urls = q->selectedUrls();
2564         QList<QUrl>::ConstIterator it = urls.begin();
2565         for (; it != urls.end() && atmost > 0; ++it) {
2566             if ((*it).isValid()) {
2567                 KRecentDocument::add(*it);
2568                 atmost--;
2569             }
2570         }
2571     }
2572 }
2573 
2574 KUrlComboBox *KFileWidget::locationEdit() const
2575 {
2576     return d->m_locationEdit;
2577 }
2578 
2579 KFileFilterCombo *KFileWidget::filterWidget() const
2580 {
2581     return d->m_filterWidget;
2582 }
2583 
2584 void KFileWidgetPrivate::togglePlacesPanel(bool show, QObject *sender)
2585 {
2586     if (show) {
2587         initPlacesPanel();
2588         m_placesDock->show();
2589 
2590         // check to see if they have a home item defined, if not show the home button
2591         QUrl homeURL;
2592         homeURL.setPath(QDir::homePath());
2593         KFilePlacesModel *model = static_cast<KFilePlacesModel *>(m_placesView->model());
2594         for (int rowIndex = 0; rowIndex < model->rowCount(); rowIndex++) {
2595             QModelIndex index = model->index(rowIndex, 0);
2596             QUrl url = model->url(index);
2597 
2598             if (homeURL.matches(url, QUrl::StripTrailingSlash)) {
2599                 m_toolbar->removeAction(m_ops->action(KDirOperator::Home));
2600                 break;
2601             }
2602         }
2603     } else {
2604         if (sender == m_placesDock && m_placesDock && m_placesDock->isVisibleTo(q)) {
2605             // we didn't *really* go away! the dialog was simply hidden or
2606             // we changed virtual desktops or ...
2607             return;
2608         }
2609 
2610         if (m_placesDock) {
2611             m_placesDock->hide();
2612         }
2613 
2614         QAction *homeAction = m_ops->action(KDirOperator::Home);
2615         QAction *reloadAction = m_ops->action(KDirOperator::Reload);
2616         if (!m_toolbar->actions().contains(homeAction)) {
2617             m_toolbar->insertAction(reloadAction, homeAction);
2618         }
2619     }
2620 
2621     m_togglePlacesPanelAction->setChecked(show);
2622 
2623     // if we don't show the places panel, at least show the places menu
2624     m_urlNavigator->setPlacesSelectorVisible(!show);
2625 }
2626 
2627 void KFileWidgetPrivate::toggleBookmarks(bool show)
2628 {
2629     if (show) {
2630         if (m_bookmarkHandler) {
2631             return;
2632         }
2633         m_bookmarkHandler = new KFileBookmarkHandler(q);
2634         q->connect(m_bookmarkHandler, &KFileBookmarkHandler::openUrl, q, [this](const QString &path) {
2635             enterUrl(path);
2636         });
2637         m_bookmarkButton->setMenu(m_bookmarkHandler->menu());
2638     } else if (m_bookmarkHandler) {
2639         m_bookmarkButton->setMenu(nullptr);
2640         delete m_bookmarkHandler;
2641         m_bookmarkHandler = nullptr;
2642     }
2643 
2644     if (m_bookmarkButton) {
2645         m_bookmarkButton->setVisible(show);
2646     }
2647 
2648     m_toggleBookmarksAction->setChecked(show);
2649 }
2650 
2651 // static, overloaded
2652 QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass)
2653 {
2654     QString fileName; // result discarded
2655     return getStartUrl(startDir, recentDirClass, fileName);
2656 }
2657 
2658 // static, overloaded
2659 QUrl KFileWidget::getStartUrl(const QUrl &startDir, QString &recentDirClass, QString &fileName)
2660 {
2661     recentDirClass.clear();
2662     fileName.clear();
2663     QUrl ret;
2664 
2665     bool useDefaultStartDir = startDir.isEmpty();
2666     if (!useDefaultStartDir) {
2667         if (startDir.scheme() == QLatin1String("kfiledialog")) {
2668             //  The startDir URL with this protocol may be in the format:
2669             //                                                    directory()   fileName()
2670             //  1.  kfiledialog:///keyword                           "/"         keyword
2671             //  2.  kfiledialog:///keyword?global                    "/"         keyword
2672             //  3.  kfiledialog:///keyword/                          "/"         keyword
2673             //  4.  kfiledialog:///keyword/?global                   "/"         keyword
2674             //  5.  kfiledialog:///keyword/filename                /keyword      filename
2675             //  6.  kfiledialog:///keyword/filename?global         /keyword      filename
2676 
2677             QString keyword;
2678             QString urlDir = startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path();
2679             QString urlFile = startDir.fileName();
2680             if (urlDir == QLatin1String("/")) { // '1'..'4' above
2681                 keyword = urlFile;
2682                 fileName.clear();
2683             } else { // '5' or '6' above
2684                 keyword = urlDir.mid(1);
2685                 fileName = urlFile;
2686             }
2687 
2688             const QLatin1String query(":%1");
2689             recentDirClass = query.arg(keyword);
2690 
2691             ret = QUrl::fromLocalFile(KRecentDirs::dir(recentDirClass));
2692         } else { // not special "kfiledialog" URL
2693             // "foo.png" only gives us a file name, the default start dir will be used.
2694             // "file:foo.png" (from KHTML/webkit, due to fromPath()) means the same
2695             //   (and is the reason why we don't just use QUrl::isRelative()).
2696 
2697             // In all other cases (startDir contains a directory path, or has no
2698             // fileName for us anyway, such as smb://), startDir is indeed a dir url.
2699 
2700             if (!startDir.adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash).path().isEmpty() || startDir.fileName().isEmpty()) {
2701                 // can use start directory
2702                 ret = startDir; // will be checked by stat later
2703                 // If we won't be able to list it (e.g. http), then use default
2704                 if (!KProtocolManager::supportsListing(ret)) {
2705                     useDefaultStartDir = true;
2706                     fileName = startDir.fileName();
2707                 }
2708             } else { // file name only
2709                 fileName = startDir.fileName();
2710                 useDefaultStartDir = true;
2711             }
2712         }
2713     }
2714 
2715     if (useDefaultStartDir) {
2716         if (lastDirectory()->isEmpty()) {
2717             *lastDirectory() = QUrl::fromLocalFile(QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation));
2718             const QUrl home(QUrl::fromLocalFile(QDir::homePath()));
2719             // if there is no docpath set (== home dir), we prefer the current
2720             // directory over it. We also prefer the homedir when our CWD is
2721             // different from our homedirectory or when the document dir
2722             // does not exist
2723             if (lastDirectory()->adjusted(QUrl::StripTrailingSlash) == home.adjusted(QUrl::StripTrailingSlash) //
2724                 || QDir::currentPath() != QDir::homePath() //
2725                 || !QDir(lastDirectory()->toLocalFile()).exists()) {
2726                 *lastDirectory() = QUrl::fromLocalFile(QDir::currentPath());
2727             }
2728         }
2729         ret = *lastDirectory();
2730     }
2731 
2732     // qDebug() << "for" << startDir << "->" << ret << "recentDirClass" << recentDirClass << "fileName" << fileName;
2733     return ret;
2734 }
2735 
2736 void KFileWidget::setStartDir(const QUrl &directory)
2737 {
2738     if (directory.isValid()) {
2739         *lastDirectory() = directory;
2740     }
2741 }
2742 
2743 void KFileWidgetPrivate::setNonExtSelection()
2744 {
2745     // Enhanced rename: Don't highlight the file extension.
2746     QString filename = locationEditCurrentText();
2747     QMimeDatabase db;
2748     QString extension = db.suffixForFileName(filename);
2749 
2750     if (!extension.isEmpty()) {
2751         m_locationEdit->lineEdit()->setSelection(0, filename.length() - extension.length() - 1);
2752     } else {
2753         int lastDot = filename.lastIndexOf(QLatin1Char('.'));
2754         if (lastDot > 0) {
2755             m_locationEdit->lineEdit()->setSelection(0, lastDot);
2756         } else {
2757             m_locationEdit->lineEdit()->selectAll();
2758         }
2759     }
2760 }
2761 
2762 // Sets the filter text to "File type" if the dialog is saving and a MIME type
2763 // filter has been set; otherwise, the text is "Filter:"
2764 void KFileWidgetPrivate::updateFilterText()
2765 {
2766     QString label;
2767     QString whatsThisText;
2768 
2769     if (m_operationMode == KFileWidget::Saving && !m_filterWidget->currentFilter().mimePatterns().isEmpty()) {
2770         label = i18n("&File type:");
2771         whatsThisText = i18n("<qt>This is the file type selector. It is used to select the format that the file will be saved as.</qt>");
2772     } else {
2773         label = i18n("&Filter:");
2774         whatsThisText = i18n(
2775             "<qt>This is the filter to apply to the file list. "
2776             "File names that do not match the filter will not be shown.<p>"
2777             "You may select from one of the preset filters in the "
2778             "drop down menu, or you may enter a custom filter "
2779             "directly into the text area.</p><p>"
2780             "Wildcards such as * and ? are allowed.</p></qt>");
2781     }
2782 
2783     if (m_filterLabel) {
2784         m_filterLabel->setText(label);
2785         m_filterLabel->setWhatsThis(whatsThisText);
2786     }
2787     if (m_filterWidget) {
2788         m_filterWidget->setWhatsThis(whatsThisText);
2789     }
2790 }
2791 
2792 void KFileWidget::setCustomWidget(QWidget *widget)
2793 {
2794     delete d->m_bottomCustomWidget;
2795     d->m_bottomCustomWidget = widget;
2796 
2797     // add it to the dialog, below the filter list box.
2798 
2799     // Change the parent so that this widget is a child of the main widget
2800     d->m_bottomCustomWidget->setParent(this);
2801 
2802     d->m_opsWidgetLayout->addWidget(d->m_bottomCustomWidget);
2803 
2804     // FIXME: This should adjust the tab orders so that the custom widget
2805     // comes after the Cancel button. The code appears to do this, but the result
2806     // somehow screws up the tab order of the file path combo box. Not a major
2807     // problem, but ideally the tab order with a custom widget should be
2808     // the same as the order without one.
2809     setTabOrder(d->m_cancelButton, d->m_bottomCustomWidget);
2810     setTabOrder(d->m_bottomCustomWidget, d->m_urlNavigator);
2811 }
2812 
2813 void KFileWidget::setCustomWidget(const QString &text, QWidget *widget)
2814 {
2815     delete d->m_labeledCustomWidget;
2816     d->m_labeledCustomWidget = widget;
2817 
2818     QLabel *label = new QLabel(text, this);
2819     label->setAlignment(Qt::AlignRight);
2820     d->m_lafBox->addRow(label, widget);
2821 }
2822 
2823 KDirOperator *KFileWidget::dirOperator()
2824 {
2825     return d->m_ops;
2826 }
2827 
2828 void KFileWidget::readConfig(KConfigGroup &group)
2829 {
2830     d->m_configGroup = group;
2831     d->readViewConfig();
2832     d->readRecentFiles();
2833 }
2834 
2835 QString KFileWidgetPrivate::locationEditCurrentText() const
2836 {
2837     return QDir::fromNativeSeparators(m_locationEdit->currentText());
2838 }
2839 
2840 QUrl KFileWidgetPrivate::mostLocalUrl(const QUrl &url)
2841 {
2842     if (url.isLocalFile()) {
2843         return url;
2844     }
2845 
2846     KIO::StatJob *statJob = KIO::stat(url, KIO::HideProgressInfo);
2847     KJobWidgets::setWindow(statJob, q);
2848     bool res = statJob->exec();
2849 
2850     if (!res) {
2851         return url;
2852     }
2853 
2854     const QString path = statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH);
2855     if (!path.isEmpty()) {
2856         QUrl newUrl;
2857         newUrl.setPath(path);
2858         return newUrl;
2859     }
2860 
2861     return url;
2862 }
2863 
2864 void KFileWidgetPrivate::setInlinePreviewShown(bool show)
2865 {
2866     m_ops->setInlinePreviewShown(show);
2867 }
2868 
2869 void KFileWidget::setConfirmOverwrite(bool enable)
2870 {
2871     d->m_confirmOverwrite = enable;
2872 }
2873 
2874 void KFileWidget::setInlinePreviewShown(bool show)
2875 {
2876     d->setInlinePreviewShown(show);
2877 }
2878 
2879 QSize KFileWidget::dialogSizeHint() const
2880 {
2881     int fontSize = fontMetrics().height();
2882     QSize goodSize(48 * fontSize, 30 * fontSize);
2883     const QSize scrnSize = d->screenSize();
2884     QSize minSize(scrnSize / 2);
2885     QSize maxSize(scrnSize * qreal(0.9));
2886     return (goodSize.expandedTo(minSize).boundedTo(maxSize));
2887 }
2888 
2889 void KFileWidget::setViewMode(KFile::FileView mode)
2890 {
2891     d->m_ops->setViewMode(mode);
2892     d->m_hasView = true;
2893 }
2894 
2895 void KFileWidget::setSupportedSchemes(const QStringList &schemes)
2896 {
2897     d->m_model->setSupportedSchemes(schemes);
2898     d->m_ops->setSupportedSchemes(schemes);
2899     d->m_urlNavigator->setSupportedSchemes(schemes);
2900 }
2901 
2902 QStringList KFileWidget::supportedSchemes() const
2903 {
2904     return d->m_model->supportedSchemes();
2905 }
2906 
2907 #include "moc_kfilewidget.cpp"