File indexing completed on 2024-04-28 15:26:47

0001 /*
0002     This file is part of the KDE project
0003     SPDX-FileCopyrightText: 1998-2009 David Faure <faure@kde.org>
0004     SPDX-FileCopyrightText: 2003 Sven Leiber <s.leiber@web.de>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only
0007 */
0008 
0009 #include "knewfilemenu.h"
0010 #include "../utils_p.h"
0011 #include "knameandurlinputdialog.h"
0012 
0013 #include <kdirnotify.h>
0014 #include <kio/copyjob.h>
0015 #include <kio/fileundomanager.h>
0016 #include <kio/jobuidelegate.h>
0017 #include <kio/mkdirjob.h>
0018 #include <kio/mkpathjob.h>
0019 #include <kio/namefinderjob.h>
0020 #include <kio/statjob.h>
0021 #include <kio/storedtransferjob.h>
0022 #include <kpropertiesdialog.h>
0023 #include <kprotocolinfo.h>
0024 #include <kprotocolmanager.h>
0025 #include <krun.h>
0026 #include <kurifilter.h>
0027 
0028 #if KIOFILEWIDGETS_BUILD_DEPRECATED_SINCE(5, 100)
0029 #include <KActionCollection>
0030 #endif
0031 #include <KConfigGroup>
0032 #include <KDesktopFile>
0033 #include <KDirOperator>
0034 #include <KDirWatch>
0035 #include <KFileUtils>
0036 #include <KJobWidgets>
0037 #include <KLocalizedString>
0038 #include <KMessageBox>
0039 #include <KMessageWidget>
0040 #include <KShell>
0041 
0042 #include <QActionGroup>
0043 #include <QDebug>
0044 #include <QDialog>
0045 #include <QDialogButtonBox>
0046 #include <QDir>
0047 #include <QLabel>
0048 #include <QLineEdit>
0049 #include <QList>
0050 #include <QMenu>
0051 #include <QMimeDatabase>
0052 #include <QPushButton>
0053 #include <QStandardPaths>
0054 #include <QTemporaryFile>
0055 #include <QTimer>
0056 #include <QVBoxLayout>
0057 
0058 #ifdef Q_OS_WIN
0059 #include <sys/utime.h>
0060 #else
0061 #include <utime.h>
0062 #endif
0063 
0064 #include <set>
0065 
0066 static QString expandTilde(const QString &name, bool isfile = false)
0067 {
0068     if (name.isEmpty() || name == QLatin1Char('~')) {
0069         return name;
0070     }
0071 
0072     QString expandedName;
0073     if (!isfile || name[0] == QLatin1Char('\\')) {
0074         expandedName = KShell::tildeExpand(name);
0075     }
0076 
0077     // If a tilde mark cannot be properly expanded, KShell::tildeExpand returns an empty string
0078     return !expandedName.isEmpty() ? expandedName : name;
0079 }
0080 
0081 // Singleton, with data shared by all KNewFileMenu instances
0082 class KNewFileMenuSingleton
0083 {
0084 public:
0085     KNewFileMenuSingleton()
0086         : dirWatch(nullptr)
0087         , filesParsed(false)
0088         , templatesList(nullptr)
0089         , templatesVersion(0)
0090     {
0091     }
0092 
0093     ~KNewFileMenuSingleton()
0094     {
0095         delete templatesList;
0096     }
0097 
0098     /**
0099      * Opens the desktop files and completes the Entry list
0100      * Input: the entry list. Output: the entry list ;-)
0101      */
0102     void parseFiles();
0103 
0104     enum EntryType {
0105         Unknown = 0, // Not parsed, i.e. we don't know
0106         LinkToTemplate, // A desktop file that points to a file or dir to copy
0107         Template, // A real file to copy as is (the KDE-1.x solution)
0108     };
0109 
0110     std::unique_ptr<KDirWatch> dirWatch;
0111 
0112     struct Entry {
0113         QString text;
0114         QString filePath;
0115         QString templatePath; // same as filePath for Template
0116         QString icon;
0117         EntryType entryType;
0118         QString comment;
0119         QString mimeType;
0120     };
0121     // NOTE: only filePath is known before we call parseFiles
0122 
0123     /**
0124      * List of all template files. It is important that they are in
0125      * the same order as the 'New' menu.
0126      */
0127     typedef QList<Entry> EntryList;
0128 
0129     /**
0130      * Set back to false each time new templates are found,
0131      * and to true on the first call to parseFiles
0132      */
0133     bool filesParsed;
0134     EntryList *templatesList;
0135 
0136     /**
0137      * Is increased when templatesList has been updated and
0138      * menu needs to be re-filled. Menus have their own version and compare it
0139      * to templatesVersion before showing up
0140      */
0141     int templatesVersion;
0142 };
0143 
0144 void KNewFileMenuSingleton::parseFiles()
0145 {
0146     // qDebug();
0147     filesParsed = true;
0148     QMutableListIterator templIter(*templatesList);
0149     while (templIter.hasNext()) {
0150         KNewFileMenuSingleton::Entry &templ = templIter.next();
0151         const QString &filePath = templ.filePath;
0152         QString text;
0153         QString templatePath;
0154         // If a desktop file, then read the name from it.
0155         // Otherwise (or if no name in it?) use file name
0156         if (KDesktopFile::isDesktopFile(filePath)) {
0157             KDesktopFile desktopFile(filePath);
0158             if (desktopFile.noDisplay()) {
0159                 templIter.remove();
0160                 continue;
0161             }
0162 
0163             text = desktopFile.readName();
0164             templ.icon = desktopFile.readIcon();
0165             templ.comment = desktopFile.readComment();
0166             if (desktopFile.readType() == QLatin1String("Link")) {
0167                 templatePath = desktopFile.desktopGroup().readPathEntry("URL", QString());
0168                 if (templatePath.startsWith(QLatin1String("file:/"))) {
0169                     templatePath = QUrl(templatePath).toLocalFile();
0170                 } else if (!templatePath.startsWith(QLatin1Char('/')) && !templatePath.startsWith(QLatin1String("__"))) {
0171                     // A relative path, then (that's the default in the files we ship)
0172                     const QStringView linkDir = QStringView(filePath).left(filePath.lastIndexOf(QLatin1Char('/')) + 1 /*keep / */);
0173                     // qDebug() << "linkDir=" << linkDir;
0174                     templatePath = linkDir + templatePath;
0175                 }
0176             }
0177             if (templatePath.isEmpty()) {
0178                 // No URL key, this is an old-style template
0179                 templ.entryType = KNewFileMenuSingleton::Template;
0180                 templ.templatePath = templ.filePath; // we'll copy the file
0181             } else {
0182                 templ.entryType = KNewFileMenuSingleton::LinkToTemplate;
0183                 templ.templatePath = templatePath;
0184             }
0185         }
0186         if (text.isEmpty()) {
0187             text = QUrl(filePath).fileName();
0188             const QLatin1String suffix(".desktop");
0189             if (text.endsWith(suffix)) {
0190                 text.chop(suffix.size());
0191             }
0192         }
0193         templ.text = text;
0194         /*// qDebug() << "Updating entry with text=" << text
0195                         << "entryType=" << templ.entryType
0196                         << "templatePath=" << templ.templatePath;*/
0197     }
0198 }
0199 
0200 Q_GLOBAL_STATIC(KNewFileMenuSingleton, kNewMenuGlobals)
0201 
0202 class KNewFileMenuCopyData
0203 {
0204 public:
0205     KNewFileMenuCopyData()
0206     {
0207         m_isSymlink = false;
0208     }
0209     QString chosenFileName() const
0210     {
0211         return m_chosenFileName;
0212     }
0213 
0214     // If empty, no copy is performed.
0215     QString sourceFileToCopy() const
0216     {
0217         return m_src;
0218     }
0219     QString tempFileToDelete() const
0220     {
0221         return m_tempFileToDelete;
0222     }
0223     bool m_isSymlink;
0224 
0225     QString m_chosenFileName;
0226     QString m_src;
0227     QString m_tempFileToDelete;
0228     QString m_templatePath;
0229 };
0230 
0231 class KNewFileMenuPrivate
0232 {
0233 public:
0234     explicit KNewFileMenuPrivate(KNewFileMenu *qq)
0235         : q(qq)
0236         , m_delayedSlotTextChangedTimer(new QTimer(q))
0237     {
0238         m_delayedSlotTextChangedTimer->setInterval(50);
0239         m_delayedSlotTextChangedTimer->setSingleShot(true);
0240     }
0241 
0242     bool checkSourceExists(const QString &src);
0243 
0244     /**
0245      * The strategy used for other desktop files than Type=Link. Example: Application, Device.
0246      */
0247     void executeOtherDesktopFile(const KNewFileMenuSingleton::Entry &entry);
0248 
0249     /**
0250      * The strategy used for "real files or directories" (the common case)
0251      */
0252     void executeRealFileOrDir(const KNewFileMenuSingleton::Entry &entry);
0253 
0254     /**
0255      * Actually performs file handling. Reads in m_copyData for needed data, that has been collected by execute*() before
0256      */
0257     void executeStrategy();
0258 
0259     /**
0260      * The strategy used when creating a symlink
0261      */
0262     void executeSymLink(const KNewFileMenuSingleton::Entry &entry);
0263 
0264     /**
0265      * The strategy used for "url" desktop files
0266      */
0267     void executeUrlDesktopFile(const KNewFileMenuSingleton::Entry &entry);
0268 
0269     /**
0270      * Fills the menu from the templates list.
0271      */
0272     void fillMenu();
0273 
0274     /**
0275      * Tries to map a local URL for the given URL.
0276      */
0277     QUrl mostLocalUrl(const QUrl &url);
0278 
0279     /**
0280      * Just clears the string buffer d->m_text, but I need a slot for this to occur
0281      */
0282     void slotAbortDialog();
0283 
0284     /**
0285      * Called when New->* is clicked
0286      */
0287     void slotActionTriggered(QAction *action);
0288 
0289     /**
0290      * Shows a dialog asking the user to enter a name when creating a new folder.
0291      */
0292     void showNewDirNameDlg(const QString &name);
0293 
0294     /**
0295      * Callback function that reads in directory name from dialog and processes it
0296      */
0297     void slotCreateDirectory();
0298 
0299     /**
0300      * Fills the templates list.
0301      */
0302     void slotFillTemplates();
0303 
0304     /**
0305      * Called when accepting the KPropertiesDialog (for "other desktop files")
0306      */
0307     void _k_slotOtherDesktopFile(KPropertiesDialog *sender);
0308 
0309     /**
0310      * Called when closing the KPropertiesDialog is closed (whichever way, accepted and rejected)
0311      */
0312     void slotOtherDesktopFileClosed();
0313 
0314     /**
0315      * Callback in KNewFileMenu for the RealFile Dialog. Handles dialog input and gives over
0316      * to executeStrategy()
0317      */
0318     void slotRealFileOrDir();
0319 
0320     /**
0321      * Delay calls to _k_slotTextChanged
0322      */
0323     void _k_delayedSlotTextChanged();
0324 
0325     /**
0326      * Dialogs use this slot to write the changed string into KNewFile menu when the user
0327      * changes touches them
0328      */
0329     void _k_slotTextChanged(const QString &text);
0330 
0331     /**
0332      * Callback in KNewFileMenu for the Symlink Dialog. Handles dialog input and gives over
0333      * to executeStrategy()
0334      */
0335     void slotSymLink();
0336 
0337     /**
0338      * Callback in KNewFileMenu for the Url/Desktop Dialog. Handles dialog input and gives over
0339      * to executeStrategy()
0340      */
0341     void slotUrlDesktopFile();
0342 
0343     /**
0344      * Callback to check if a file/directory with the same name as the one being created, exists
0345      */
0346     void _k_slotStatResult(KJob *job);
0347 
0348     void _k_slotAccepted();
0349 
0350     /**
0351      * Initializes m_fileDialog and the other widgets that are included in it. Mainly to reduce
0352      * code duplication in showNewDirNameDlg() and executeRealFileOrDir().
0353      */
0354     void initDialog();
0355 
0356 #if KIOFILEWIDGETS_BUILD_DEPRECATED_SINCE(5, 100)
0357     KActionCollection *m_actionCollection = nullptr;
0358 #endif
0359     QAction *m_newFolderShortcutAction = nullptr;
0360     QAction *m_newFileShortcutAction = nullptr;
0361 
0362     KActionMenu *m_menuDev = nullptr;
0363     int m_menuItemsVersion = 0;
0364     QAction *m_newDirAction = nullptr;
0365     QDialog *m_fileDialog = nullptr;
0366     KMessageWidget *m_messageWidget = nullptr;
0367     QLabel *m_label = nullptr;
0368     QLineEdit *m_lineEdit = nullptr;
0369     QDialogButtonBox *m_buttonBox = nullptr;
0370 
0371     // This is used to allow _k_slotTextChanged to know whether it's being used to
0372     // create a file or a directory without duplicating code across two functions
0373     bool m_creatingDirectory = false;
0374     bool m_modal = true;
0375 
0376     /**
0377      * The action group that our actions belong to
0378      */
0379     QActionGroup *m_newMenuGroup = nullptr;
0380     QWidget *m_parentWidget = nullptr;
0381 
0382     /**
0383      * When the user pressed the right mouse button over an URL a popup menu
0384      * is displayed. The URL belonging to this popup menu is stored here.
0385      * For all intents and purposes this is the current directory where the menu is
0386      * opened.
0387      * TODO KF6 make it a single QUrl.
0388      */
0389     QList<QUrl> m_popupFiles;
0390 
0391     QStringList m_supportedMimeTypes;
0392     QString m_tempFileToDelete; // set when a tempfile was created for a Type=URL desktop file
0393     QString m_text;
0394 
0395     KNewFileMenuSingleton::Entry *m_firstFileEntry = nullptr;
0396 
0397     KNewFileMenu *const q;
0398 
0399     KNewFileMenuCopyData m_copyData;
0400 
0401     /**
0402      * Use to delay a bit feedback to user
0403      */
0404     QTimer *m_delayedSlotTextChangedTimer;
0405 
0406     QUrl m_baseUrl;
0407 
0408     bool m_selectDirWhenAlreadyExists = false;
0409     bool m_acceptedPressed = false;
0410     bool m_statRunning = false;
0411 };
0412 
0413 void KNewFileMenuPrivate::_k_slotAccepted()
0414 {
0415     if (m_statRunning || m_delayedSlotTextChangedTimer->isActive()) {
0416         // stat is running or _k_slotTextChanged has not been called already
0417         // delay accept until stat has been run
0418         m_acceptedPressed = true;
0419 
0420         if (m_delayedSlotTextChangedTimer->isActive()) {
0421             m_delayedSlotTextChangedTimer->stop();
0422             _k_slotTextChanged(m_lineEdit->text());
0423         }
0424     } else {
0425         m_fileDialog->accept();
0426     }
0427 }
0428 
0429 void KNewFileMenuPrivate::initDialog()
0430 {
0431     m_fileDialog = new QDialog(m_parentWidget);
0432     m_fileDialog->setAttribute(Qt::WA_DeleteOnClose);
0433     m_fileDialog->setModal(m_modal);
0434     m_fileDialog->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
0435 
0436     m_messageWidget = new KMessageWidget(m_fileDialog);
0437     m_messageWidget->setCloseButtonVisible(false);
0438     m_messageWidget->setWordWrap(true);
0439     m_messageWidget->hide();
0440 
0441     m_label = new QLabel(m_fileDialog);
0442 
0443     m_lineEdit = new QLineEdit(m_fileDialog);
0444     m_lineEdit->setClearButtonEnabled(true);
0445     m_lineEdit->setMinimumWidth(400);
0446 
0447     m_buttonBox = new QDialogButtonBox(m_fileDialog);
0448     m_buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
0449     QObject::connect(m_buttonBox, &QDialogButtonBox::accepted, [this]() {
0450         _k_slotAccepted();
0451     });
0452     QObject::connect(m_buttonBox, &QDialogButtonBox::rejected, m_fileDialog, &QDialog::reject);
0453 
0454     QObject::connect(m_fileDialog, &QDialog::finished, m_fileDialog, [this] {
0455         m_statRunning = false;
0456     });
0457 
0458     QVBoxLayout *layout = new QVBoxLayout(m_fileDialog);
0459     layout->setSizeConstraint(QLayout::SetFixedSize);
0460 
0461     layout->addWidget(m_label);
0462     layout->addWidget(m_lineEdit);
0463     layout->addWidget(m_buttonBox);
0464     layout->addWidget(m_messageWidget);
0465     layout->addStretch();
0466 }
0467 
0468 bool KNewFileMenuPrivate::checkSourceExists(const QString &src)
0469 {
0470     if (!QFile::exists(src)) {
0471         qWarning() << src << "doesn't exist";
0472 
0473         QDialog *dialog = new QDialog(m_parentWidget);
0474         dialog->setWindowTitle(i18n("Sorry"));
0475         dialog->setObjectName(QStringLiteral("sorry"));
0476         dialog->setModal(q->isModal());
0477         dialog->setAttribute(Qt::WA_DeleteOnClose);
0478 
0479         QDialogButtonBox *box = new QDialogButtonBox(dialog);
0480         box->setStandardButtons(QDialogButtonBox::Ok);
0481 
0482         KMessageBox::createKMessageBox(dialog,
0483                                        box,
0484                                        QMessageBox::Warning,
0485                                        i18n("<qt>The template file <b>%1</b> does not exist.</qt>", src),
0486                                        QStringList(),
0487                                        QString(),
0488                                        nullptr,
0489                                        KMessageBox::NoExec);
0490 
0491         dialog->show();
0492 
0493         return false;
0494     }
0495     return true;
0496 }
0497 
0498 void KNewFileMenuPrivate::executeOtherDesktopFile(const KNewFileMenuSingleton::Entry &entry)
0499 {
0500     if (!checkSourceExists(entry.templatePath)) {
0501         return;
0502     }
0503 
0504     for (const auto &url : std::as_const(m_popupFiles)) {
0505         QString text = entry.text;
0506         text.remove(QStringLiteral("...")); // the ... is fine for the menu item but not for the default filename
0507         text = text.trimmed(); // In some languages, there is a space in front of "...", see bug 268895
0508         // KDE5 TODO: remove the "..." from link*.desktop files and use i18n("%1...") when making
0509         // the action.
0510         QString name = text;
0511         text.append(QStringLiteral(".desktop"));
0512 
0513         const QUrl directory = mostLocalUrl(url);
0514         const QUrl defaultFile = QUrl::fromLocalFile(directory.toLocalFile() + QLatin1Char('/') + KIO::encodeFileName(text));
0515         if (defaultFile.isLocalFile() && QFile::exists(defaultFile.toLocalFile())) {
0516             text = KFileUtils::suggestName(directory, text);
0517         }
0518 
0519         QUrl templateUrl;
0520         bool usingTemplate = false;
0521         if (entry.templatePath.startsWith(QLatin1String(":/"))) {
0522             QTemporaryFile *tmpFile = QTemporaryFile::createNativeFile(entry.templatePath);
0523             tmpFile->setAutoRemove(false);
0524             QString tempFileName = tmpFile->fileName();
0525             tmpFile->close();
0526 
0527             KDesktopFile df(tempFileName);
0528             KConfigGroup group = df.desktopGroup();
0529             group.writeEntry("Name", name);
0530             templateUrl = QUrl::fromLocalFile(tempFileName);
0531             m_tempFileToDelete = tempFileName;
0532             usingTemplate = true;
0533         } else {
0534             templateUrl = QUrl::fromLocalFile(entry.templatePath);
0535         }
0536         KPropertiesDialog *dlg = new KPropertiesDialog(templateUrl, directory, text, m_parentWidget);
0537         dlg->setModal(q->isModal());
0538         dlg->setAttribute(Qt::WA_DeleteOnClose);
0539         QObject::connect(dlg, &KPropertiesDialog::applied, q, [this, dlg]() {
0540             _k_slotOtherDesktopFile(dlg);
0541         });
0542         if (usingTemplate) {
0543             QObject::connect(dlg, &KPropertiesDialog::propertiesClosed, q, [this]() {
0544                 slotOtherDesktopFileClosed();
0545             });
0546         }
0547         dlg->show();
0548     }
0549     // We don't set m_src here -> there will be no copy, we are done.
0550 }
0551 
0552 void KNewFileMenuPrivate::executeRealFileOrDir(const KNewFileMenuSingleton::Entry &entry)
0553 {
0554     initDialog();
0555 
0556     const auto getSelectionLength = [](const QString &text) {
0557         // Select the text without MIME-type extension
0558         int selectionLength = text.length();
0559 
0560         QMimeDatabase db;
0561         const QString extension = db.suffixForFileName(text);
0562         if (extension.isEmpty()) {
0563             // For an unknown extension just exclude the extension after
0564             // the last point. This does not work for multiple extensions like
0565             // *.tar.gz but usually this is anyhow a known extension.
0566             selectionLength = text.lastIndexOf(QLatin1Char('.'));
0567 
0568             // If no point could be found, use whole text length for selection.
0569             if (selectionLength < 1) {
0570                 selectionLength = text.length();
0571             }
0572 
0573         } else {
0574             selectionLength -= extension.length() + 1;
0575         }
0576 
0577         return selectionLength;
0578     };
0579 
0580     // The template is not a desktop file
0581     // Prompt the user to set the destination filename
0582     QString text = entry.text;
0583     text.remove(QStringLiteral("...")); // the ... is fine for the menu item but not for the default filename
0584     text = text.trimmed(); // In some languages, there is a space in front of "...", see bug 268895
0585     // add the extension (from the templatePath), should work with .txt, .html and with ".tar.gz"... etc
0586     const QString fileName = entry.templatePath.mid(entry.templatePath.lastIndexOf(QLatin1Char('/')));
0587     const int dotIndex = getSelectionLength(fileName);
0588     text += dotIndex > 0 ? fileName.mid(dotIndex) : QString();
0589 
0590     m_copyData.m_src = entry.templatePath;
0591 
0592     const QUrl directory = mostLocalUrl(m_popupFiles.first());
0593     m_baseUrl = directory;
0594     const QUrl defaultFile = QUrl::fromLocalFile(directory.toLocalFile() + QLatin1Char('/') + KIO::encodeFileName(text));
0595     if (defaultFile.isLocalFile() && QFile::exists(defaultFile.toLocalFile())) {
0596         text = KFileUtils::suggestName(directory, text);
0597     }
0598 
0599     m_label->setText(entry.comment);
0600 
0601     m_lineEdit->setText(text);
0602 
0603     m_creatingDirectory = false;
0604     _k_slotTextChanged(text);
0605     QObject::connect(m_lineEdit, &QLineEdit::textChanged, q, [this]() {
0606         _k_delayedSlotTextChanged();
0607     });
0608     m_delayedSlotTextChangedTimer->callOnTimeout(m_lineEdit, [this]() {
0609         _k_slotTextChanged(m_lineEdit->text());
0610     });
0611 
0612     QObject::connect(m_fileDialog, &QDialog::accepted, q, [this]() {
0613         slotRealFileOrDir();
0614     });
0615     QObject::connect(m_fileDialog, &QDialog::rejected, q, [this]() {
0616         slotAbortDialog();
0617     });
0618 
0619     m_fileDialog->show();
0620 
0621     const int firstDotInBaseName = getSelectionLength(text);
0622     m_lineEdit->setSelection(0, firstDotInBaseName > 0 ? firstDotInBaseName : text.size());
0623 
0624     m_lineEdit->setFocus();
0625 }
0626 
0627 void KNewFileMenuPrivate::executeSymLink(const KNewFileMenuSingleton::Entry &entry)
0628 {
0629     KNameAndUrlInputDialog *dlg = new KNameAndUrlInputDialog(i18n("Name for new link:"), entry.comment, m_popupFiles.first(), m_parentWidget);
0630     dlg->setModal(q->isModal());
0631     dlg->setAttribute(Qt::WA_DeleteOnClose);
0632     dlg->setWindowTitle(i18n("Create Symlink"));
0633     m_fileDialog = dlg;
0634     QObject::connect(dlg, &QDialog::accepted, q, [this]() {
0635         slotSymLink();
0636     });
0637     dlg->show();
0638 }
0639 
0640 void KNewFileMenuPrivate::executeStrategy()
0641 {
0642     m_tempFileToDelete = m_copyData.tempFileToDelete();
0643     const QString src = m_copyData.sourceFileToCopy();
0644     QString chosenFileName = expandTilde(m_copyData.chosenFileName(), true);
0645 
0646     if (src.isEmpty()) {
0647         return;
0648     }
0649     QUrl uSrc(QUrl::fromLocalFile(src));
0650 
0651     // In case the templates/.source directory contains symlinks, resolve
0652     // them to the target files. Fixes bug #149628.
0653     KFileItem item(uSrc, QString(), KFileItem::Unknown);
0654     if (item.isLink()) {
0655         uSrc.setPath(item.linkDest());
0656     }
0657 
0658     // The template is not a desktop file [or it's a URL one] >>> Copy it
0659     for (const auto &u : std::as_const(m_popupFiles)) {
0660         QUrl dest = u;
0661         dest.setPath(Utils::concatPaths(dest.path(), KIO::encodeFileName(chosenFileName)));
0662 
0663         QList<QUrl> lstSrc;
0664         lstSrc.append(uSrc);
0665         KIO::Job *kjob;
0666         if (m_copyData.m_isSymlink) {
0667             KIO::CopyJob *linkJob = KIO::linkAs(uSrc, dest);
0668             kjob = linkJob;
0669             KIO::FileUndoManager::self()->recordCopyJob(linkJob);
0670         } else if (src.startsWith(QLatin1String(":/"))) {
0671             QFile srcFile(src);
0672             if (!srcFile.open(QIODevice::ReadOnly)) {
0673                 return;
0674             }
0675             // The QFile won't live long enough for the job, so let's buffer the contents
0676             const QByteArray srcBuf(srcFile.readAll());
0677             KIO::StoredTransferJob *putJob = KIO::storedPut(srcBuf, dest, -1);
0678             kjob = putJob;
0679             KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Put, QList<QUrl>(), dest, putJob);
0680         } else {
0681             // qDebug() << "KIO::copyAs(" << uSrc.url() << "," << dest.url() << ")";
0682             KIO::CopyJob *job = KIO::copyAs(uSrc, dest);
0683             job->setDefaultPermissions(true);
0684             kjob = job;
0685             KIO::FileUndoManager::self()->recordCopyJob(job);
0686         }
0687         KJobWidgets::setWindow(kjob, m_parentWidget);
0688         QObject::connect(kjob, &KJob::result, q, &KNewFileMenu::slotResult);
0689     }
0690 }
0691 
0692 void KNewFileMenuPrivate::executeUrlDesktopFile(const KNewFileMenuSingleton::Entry &entry)
0693 {
0694     KNameAndUrlInputDialog *dlg = new KNameAndUrlInputDialog(i18n("Name for new link:"), entry.comment, m_popupFiles.first(), m_parentWidget);
0695     m_copyData.m_templatePath = entry.templatePath;
0696     dlg->setModal(q->isModal());
0697     dlg->setAttribute(Qt::WA_DeleteOnClose);
0698     dlg->setWindowTitle(i18n("Create link to URL"));
0699     m_fileDialog = dlg;
0700     QObject::connect(dlg, &QDialog::accepted, q, [this]() {
0701         slotUrlDesktopFile();
0702     });
0703     dlg->show();
0704 }
0705 
0706 void KNewFileMenuPrivate::fillMenu()
0707 {
0708     QMenu *menu = q->menu();
0709     menu->clear();
0710     m_menuDev->menu()->clear();
0711     m_newDirAction = nullptr;
0712 
0713     std::set<QString> seenTexts;
0714     QString lastTemplatePath;
0715     // these shall be put at special positions
0716     QAction *linkURL = nullptr;
0717     QAction *linkApp = nullptr;
0718     QAction *linkPath = nullptr;
0719 
0720     KNewFileMenuSingleton *s = kNewMenuGlobals();
0721     int idx = 0;
0722     for (auto &entry : *s->templatesList) {
0723         ++idx;
0724         if (entry.entryType != KNewFileMenuSingleton::Unknown) {
0725             // There might be a .desktop for that one already, if it's a kdelnk
0726             // This assumes we read .desktop files before .kdelnk files ...
0727 
0728             // In fact, we skip any second item that has the same text as another one.
0729             // Duplicates in a menu look bad in any case.
0730             const auto [it, isInserted] = seenTexts.insert(entry.text);
0731             if (isInserted) {
0732                 // const KNewFileMenuSingleton::Entry entry = templatesList->at(i-1);
0733 
0734                 const QString templatePath = entry.templatePath;
0735                 // The best way to identify the "Create Directory", "Link to Location", "Link to Application" was the template
0736                 if (templatePath.endsWith(QLatin1String("emptydir"))) {
0737                     QAction *act = new QAction(q);
0738                     m_newDirAction = act;
0739                     act->setIcon(QIcon::fromTheme(entry.icon));
0740                     act->setText(i18nc("@item:inmenu Create New", "%1", entry.text));
0741                     act->setActionGroup(m_newMenuGroup);
0742 
0743 #if KIOFILEWIDGETS_BUILD_DEPRECATED_SINCE(5, 100)
0744                     if (m_actionCollection) {
0745                         m_newFolderShortcutAction = m_actionCollection->action(QStringLiteral("create_dir"));
0746                     }
0747 #endif
0748 
0749                     // If there is a shortcut action copy its shortcut
0750                     if (m_newFolderShortcutAction) {
0751                         act->setShortcuts(m_newFolderShortcutAction->shortcuts());
0752                         // Both actions have now the same shortcut, so this will prevent the "Ambiguous shortcut detected" dialog.
0753                         act->setShortcutContext(Qt::WidgetShortcut);
0754                         // We also need to react to shortcut changes.
0755                         QObject::connect(m_newFolderShortcutAction, &QAction::changed, act, [=]() {
0756                             act->setShortcuts(m_newFolderShortcutAction->shortcuts());
0757                         });
0758                     }
0759 
0760                     menu->addAction(act);
0761                     menu->addSeparator();
0762                 } else {
0763                     if (lastTemplatePath.startsWith(QDir::homePath()) && !templatePath.startsWith(QDir::homePath())) {
0764                         menu->addSeparator();
0765                     }
0766                     if (!m_supportedMimeTypes.isEmpty()) {
0767                         bool keep = false;
0768 
0769                         // We need to do MIME type filtering, for real files.
0770                         const bool createSymlink = entry.templatePath == QLatin1String("__CREATE_SYMLINK__");
0771                         if (createSymlink) {
0772                             keep = true;
0773                         } else if (!KDesktopFile::isDesktopFile(entry.templatePath)) {
0774                             // Determine MIME type on demand
0775                             QMimeDatabase db;
0776                             QMimeType mime;
0777                             if (entry.mimeType.isEmpty()) {
0778                                 mime = db.mimeTypeForFile(entry.templatePath);
0779                                 // qDebug() << entry.templatePath << "is" << mime.name();
0780                                 entry.mimeType = mime.name();
0781                             } else {
0782                                 mime = db.mimeTypeForName(entry.mimeType);
0783                             }
0784                             for (const QString &supportedMime : std::as_const(m_supportedMimeTypes)) {
0785                                 if (mime.inherits(supportedMime)) {
0786                                     keep = true;
0787                                     break;
0788                                 }
0789                             }
0790                         }
0791 
0792                         if (!keep) {
0793                             // qDebug() << "Not keeping" << entry.templatePath;
0794                             continue;
0795                         }
0796                     }
0797 
0798                     QAction *act = new QAction(q);
0799                     act->setData(idx);
0800                     act->setIcon(QIcon::fromTheme(entry.icon));
0801                     act->setText(i18nc("@item:inmenu Create New", "%1", entry.text));
0802                     act->setActionGroup(m_newMenuGroup);
0803 
0804                     // qDebug() << templatePath << entry.filePath;
0805 
0806                     if (templatePath.endsWith(QLatin1String("/URL.desktop"))) {
0807                         linkURL = act;
0808                     } else if (templatePath.endsWith(QLatin1String("/Program.desktop"))) {
0809                         linkApp = act;
0810                     } else if (entry.filePath.endsWith(QLatin1String("/linkPath.desktop"))) {
0811                         linkPath = act;
0812                     } else if (KDesktopFile::isDesktopFile(templatePath)) {
0813                         KDesktopFile df(templatePath);
0814                         if (df.readType() == QLatin1String("FSDevice")) {
0815                             m_menuDev->menu()->addAction(act);
0816                         } else {
0817                             menu->addAction(act);
0818                         }
0819                     } else {
0820                         if (!m_firstFileEntry) {
0821                             m_firstFileEntry = &entry;
0822 
0823 #if KIOFILEWIDGETS_BUILD_DEPRECATED_SINCE(5, 100)
0824                             if (m_actionCollection) {
0825                                 m_newFileShortcutAction = m_actionCollection->action(QStringLiteral("create_file"));
0826                             }
0827 #endif
0828 
0829                             // If there is a shortcut action copy its shortcut
0830                             if (m_newFileShortcutAction) {
0831                                 act->setShortcuts(m_newFileShortcutAction->shortcuts());
0832                                 // Both actions have now the same shortcut, so this will prevent the "Ambiguous shortcut detected" dialog.
0833                                 act->setShortcutContext(Qt::WidgetShortcut);
0834                                 // We also need to react to shortcut changes.
0835                                 QObject::connect(m_newFileShortcutAction, &QAction::changed, act, [=]() {
0836                                     act->setShortcuts(m_newFileShortcutAction->shortcuts());
0837                                 });
0838                             }
0839                         }
0840                         menu->addAction(act);
0841                     }
0842                 }
0843             }
0844             lastTemplatePath = entry.templatePath;
0845         } else { // Separate system from personal templates
0846             Q_ASSERT(entry.entryType != 0);
0847             menu->addSeparator();
0848         }
0849     }
0850 
0851     if (m_supportedMimeTypes.isEmpty()) {
0852         menu->addSeparator();
0853         if (linkURL) {
0854             menu->addAction(linkURL);
0855         }
0856         if (linkPath) {
0857             menu->addAction(linkPath);
0858         }
0859         if (linkApp) {
0860             menu->addAction(linkApp);
0861         }
0862         Q_ASSERT(m_menuDev);
0863         if (!m_menuDev->menu()->isEmpty()) {
0864             menu->addAction(m_menuDev);
0865         }
0866     }
0867 }
0868 
0869 QUrl KNewFileMenuPrivate::mostLocalUrl(const QUrl &url)
0870 {
0871     if (url.isLocalFile() || KProtocolInfo::protocolClass(url.scheme()) != QLatin1String(":local")) {
0872         return url;
0873     }
0874 
0875     KIO::StatJob *job = KIO::mostLocalUrl(url);
0876     KJobWidgets::setWindow(job, m_parentWidget);
0877 
0878     return job->exec() ? job->mostLocalUrl() : url;
0879 }
0880 
0881 void KNewFileMenuPrivate::slotAbortDialog()
0882 {
0883     m_text = QString();
0884 }
0885 
0886 void KNewFileMenuPrivate::slotActionTriggered(QAction *action)
0887 {
0888     q->trigger(); // was for kdesktop's slotNewMenuActivated() in kde3 times. Can't hurt to keep it...
0889 
0890     if (action == m_newDirAction) {
0891         q->createDirectory();
0892         return;
0893     }
0894     const int id = action->data().toInt();
0895     Q_ASSERT(id > 0);
0896 
0897     KNewFileMenuSingleton *s = kNewMenuGlobals();
0898     const KNewFileMenuSingleton::Entry entry = s->templatesList->at(id - 1);
0899 
0900     const bool createSymlink = entry.templatePath == QLatin1String("__CREATE_SYMLINK__");
0901 
0902     m_copyData = KNewFileMenuCopyData();
0903 
0904     if (createSymlink) {
0905         m_copyData.m_isSymlink = true;
0906         executeSymLink(entry);
0907     } else if (KDesktopFile::isDesktopFile(entry.templatePath)) {
0908         KDesktopFile df(entry.templatePath);
0909         if (df.readType() == QLatin1String("Link")) {
0910             executeUrlDesktopFile(entry);
0911         } else { // any other desktop file (Device, App, etc.)
0912             executeOtherDesktopFile(entry);
0913         }
0914     } else {
0915         executeRealFileOrDir(entry);
0916     }
0917 }
0918 
0919 void KNewFileMenuPrivate::slotCreateDirectory()
0920 {
0921     // Automatically trim trailing spaces since they're pretty much always
0922     // unintentional and can cause issues on Windows in shared environments
0923     while (m_text.endsWith(QLatin1Char(' '))) {
0924         m_text.chop(1);
0925     }
0926 
0927     QUrl url;
0928     QUrl baseUrl = m_popupFiles.first();
0929 
0930     QString name = expandTilde(m_text);
0931 
0932     if (!name.isEmpty()) {
0933         if (Utils::isAbsoluteLocalPath(name)) {
0934             url = QUrl::fromLocalFile(name);
0935         } else {
0936             url = baseUrl;
0937             url.setPath(Utils::concatPaths(url.path(), name));
0938         }
0939     }
0940 
0941     KIO::Job *job;
0942     if (name.contains(QLatin1Char('/'))) {
0943         // If the name contains any slashes, use mkpath so that a/b/c works.
0944         job = KIO::mkpath(url, baseUrl);
0945         KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Mkpath, QList<QUrl>(), url, job);
0946     } else {
0947         // If not, use mkdir so it will fail if the name of an existing folder was used
0948         job = KIO::mkdir(url);
0949         KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Mkdir, QList<QUrl>(), url, job);
0950     }
0951     job->setProperty("newDirectoryURL", url);
0952     job->uiDelegate()->setAutoErrorHandlingEnabled(true);
0953     KJobWidgets::setWindow(job, m_parentWidget);
0954 
0955     if (job) {
0956         // We want the error handling to be done by slotResult so that subclasses can reimplement it
0957         job->uiDelegate()->setAutoErrorHandlingEnabled(false);
0958         QObject::connect(job, &KJob::result, q, &KNewFileMenu::slotResult);
0959     }
0960     slotAbortDialog();
0961 }
0962 
0963 struct EntryInfo {
0964     QString key;
0965     QString url;
0966     KNewFileMenuSingleton::Entry entry;
0967 };
0968 
0969 static QStringList getInstalledTemplates()
0970 {
0971     QStringList list = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, QStringLiteral("templates"), QStandardPaths::LocateDirectory);
0972     // TODO KF6, use QStandardPaths::TemplatesLocation
0973 #ifdef Q_OS_UNIX
0974     QString xdgUserDirs = QStandardPaths::locate(QStandardPaths::ConfigLocation, QStringLiteral("user-dirs.dirs"), QStandardPaths::LocateFile);
0975     QFile xdgUserDirsFile(xdgUserDirs);
0976     if (!xdgUserDirs.isEmpty() && xdgUserDirsFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
0977         static const QLatin1String marker("XDG_TEMPLATES_DIR=");
0978         QString line;
0979         QTextStream in(&xdgUserDirsFile);
0980         while (!in.atEnd()) {
0981             line.clear();
0982             in.readLineInto(&line);
0983             if (line.startsWith(marker)) {
0984                 // E.g. XDG_TEMPLATES_DIR="$HOME/templates" -> $HOME/templates
0985                 line.remove(0, marker.size()).remove(QLatin1Char('"'));
0986                 line.replace(QLatin1String("$HOME"), QDir::homePath());
0987                 if (QDir(line).exists()) {
0988                     list << line;
0989                 }
0990                 break;
0991             }
0992         }
0993     }
0994 #endif
0995     return list;
0996 }
0997 
0998 static QStringList getTemplateFilePaths(const QStringList &templates)
0999 {
1000     QDir dir;
1001     QStringList files;
1002     for (const QString &path : templates) {
1003         dir.setPath(path);
1004         const QStringList entryList = dir.entryList(QStringList{QStringLiteral("*.desktop")}, QDir::Files);
1005         files.reserve(files.size() + entryList.size());
1006         for (const QString &entry : entryList) {
1007             const QString file = Utils::concatPaths(dir.path(), entry);
1008             files.append(file);
1009         }
1010     }
1011 
1012     return files;
1013 }
1014 
1015 void KNewFileMenuPrivate::slotFillTemplates()
1016 {
1017     KNewFileMenuSingleton *instance = kNewMenuGlobals();
1018     // qDebug();
1019 
1020     const QStringList installedTemplates = getInstalledTemplates();
1021     const QStringList qrcTemplates{QStringLiteral(":/kio5/newfile-templates")};
1022     const QStringList templates = qrcTemplates + installedTemplates;
1023 
1024     // Ensure any changes in the templates dir will call this
1025     if (!instance->dirWatch) {
1026         instance->dirWatch = std::make_unique<KDirWatch>();
1027         for (const QString &dir : installedTemplates) {
1028             instance->dirWatch->addDir(dir);
1029         }
1030 
1031         auto slotFunc = [this]() {
1032             slotFillTemplates();
1033         };
1034         QObject::connect(instance->dirWatch.get(), &KDirWatch::dirty, q, slotFunc);
1035         QObject::connect(instance->dirWatch.get(), &KDirWatch::created, q, slotFunc);
1036         QObject::connect(instance->dirWatch.get(), &KDirWatch::deleted, q, slotFunc);
1037         // Ok, this doesn't cope with new dirs in XDG_DATA_DIRS, but that's another story
1038     }
1039 
1040     // Look into "templates" dirs.
1041     QStringList files = getTemplateFilePaths(templates);
1042     auto removeFunc = [](const QString &path) {
1043         return path.startsWith(QLatin1Char('.'));
1044     };
1045     files.erase(std::remove_if(files.begin(), files.end(), removeFunc), files.end());
1046 
1047     std::vector<EntryInfo> uniqueEntries;
1048 
1049     for (const QString &file : files) {
1050         // qDebug() << file;
1051         KNewFileMenuSingleton::Entry entry;
1052         entry.filePath = file;
1053         entry.entryType = KNewFileMenuSingleton::Unknown; // not parsed yet
1054 
1055         // Put Directory first in the list (a bit hacky),
1056         // and TextFile before others because it's the most used one.
1057         // This also sorts by user-visible name.
1058         // The rest of the re-ordering is done in fillMenu.
1059         const KDesktopFile config(file);
1060         const QString url = config.desktopGroup().readEntry("URL");
1061         QString key = config.desktopGroup().readEntry("Name");
1062         if (file.endsWith(QLatin1String("Directory.desktop"))) {
1063             key.prepend(QLatin1Char('0'));
1064         } else if (file.startsWith(QDir::homePath())) {
1065             key.prepend(QLatin1Char('1'));
1066         } else if (file.endsWith(QLatin1String("TextFile.desktop"))) {
1067             key.prepend(QLatin1Char('2'));
1068         } else {
1069             key.prepend(QLatin1Char('3'));
1070         }
1071 
1072         EntryInfo eInfo = {key, url, entry};
1073         auto it = std::find_if(uniqueEntries.begin(), uniqueEntries.end(), [&url](const EntryInfo &info) {
1074             return url == info.url;
1075         });
1076 
1077         if (it != uniqueEntries.cend()) {
1078             *it = eInfo;
1079         } else {
1080             uniqueEntries.push_back(eInfo);
1081         }
1082     }
1083 
1084     std::sort(uniqueEntries.begin(), uniqueEntries.end(), [](const EntryInfo &a, const EntryInfo &b) {
1085         return a.key < b.key;
1086     });
1087 
1088     ++instance->templatesVersion;
1089     instance->filesParsed = false;
1090 
1091     instance->templatesList->clear();
1092 
1093     instance->templatesList->reserve(uniqueEntries.size());
1094     for (const auto &info : uniqueEntries) {
1095         instance->templatesList->append(info.entry);
1096     };
1097 }
1098 
1099 void KNewFileMenuPrivate::_k_slotOtherDesktopFile(KPropertiesDialog *sender)
1100 {
1101     // The properties dialog took care of the copying, so we're done
1102     Q_EMIT q->fileCreated(sender->url());
1103 }
1104 
1105 void KNewFileMenuPrivate::slotOtherDesktopFileClosed()
1106 {
1107     QFile::remove(m_tempFileToDelete);
1108 }
1109 
1110 void KNewFileMenuPrivate::slotRealFileOrDir()
1111 {
1112     // Automatically trim trailing spaces since they're pretty much always
1113     // unintentional and can cause issues on Windows in shared environments
1114     while (m_text.endsWith(QLatin1Char(' '))) {
1115         m_text.chop(1);
1116     }
1117     m_copyData.m_chosenFileName = m_text;
1118     slotAbortDialog();
1119     executeStrategy();
1120 }
1121 
1122 void KNewFileMenuPrivate::slotSymLink()
1123 {
1124     KNameAndUrlInputDialog *dlg = static_cast<KNameAndUrlInputDialog *>(m_fileDialog);
1125 
1126     m_copyData.m_chosenFileName = dlg->name(); // no path
1127     const QString linkTarget = dlg->urlText();
1128 
1129     if (m_copyData.m_chosenFileName.isEmpty() || linkTarget.isEmpty()) {
1130         return;
1131     }
1132 
1133     m_copyData.m_src = linkTarget;
1134     executeStrategy();
1135 }
1136 
1137 void KNewFileMenuPrivate::_k_delayedSlotTextChanged()
1138 {
1139     m_delayedSlotTextChangedTimer->start();
1140     m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!m_lineEdit->text().isEmpty());
1141 }
1142 
1143 void KNewFileMenuPrivate::_k_slotTextChanged(const QString &text)
1144 {
1145     // Validate input, displaying a KMessageWidget for questionable names
1146 
1147     if (text.isEmpty()) {
1148         m_messageWidget->hide();
1149         m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1150     }
1151 
1152     // Don't allow creating folders that would mask . or ..
1153     else if (text == QLatin1Char('.') || text == QLatin1String("..")) {
1154         m_messageWidget->setText(
1155             xi18nc("@info", "The name <filename>%1</filename> cannot be used because it is reserved for use by the operating system.", text));
1156         m_messageWidget->setMessageType(KMessageWidget::Error);
1157         m_messageWidget->animatedShow();
1158         m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1159     }
1160 
1161     // File or folder would be hidden; show warning
1162     else if (text.startsWith(QLatin1Char('.'))) {
1163         m_messageWidget->setText(xi18nc("@info", "The name <filename>%1</filename> starts with a dot, so it will be hidden by default.", text));
1164         m_messageWidget->setMessageType(KMessageWidget::Warning);
1165         m_messageWidget->animatedShow();
1166     }
1167 
1168     // File or folder begins with a space; show warning
1169     else if (text.startsWith(QLatin1Char(' '))) {
1170         m_messageWidget->setText(xi18nc("@info",
1171                                         "The name <filename>%1</filename> starts with a space, which will result in it being shown before other items when "
1172                                         "sorting alphabetically, among other potential oddities.",
1173                                         text));
1174         m_messageWidget->setMessageType(KMessageWidget::Warning);
1175         m_messageWidget->animatedShow();
1176     }
1177 #ifndef Q_OS_WIN
1178     // Inform the user that slashes in folder names create a directory tree
1179     else if (text.contains(QLatin1Char('/'))) {
1180         if (m_creatingDirectory) {
1181             QStringList folders = text.split(QLatin1Char('/'));
1182             if (!folders.isEmpty()) {
1183                 if (folders.first().isEmpty()) {
1184                     folders.removeFirst();
1185                 }
1186             }
1187             QString label;
1188             if (folders.count() > 1) {
1189                 label = i18n("Using slashes in folder names will create sub-folders, like so:");
1190                 QString indentation = QString();
1191                 for (const QString &folder : std::as_const(folders)) {
1192                     label.append(QLatin1Char('\n'));
1193                     label.append(indentation);
1194                     label.append(folder);
1195                     label.append(QStringLiteral("/"));
1196                     indentation.append(QStringLiteral("    "));
1197                 }
1198             } else {
1199                 label = i18n("Using slashes in folder names will create sub-folders.");
1200             }
1201             m_messageWidget->setText(label);
1202             m_messageWidget->setMessageType(KMessageWidget::Information);
1203             m_messageWidget->animatedShow();
1204         }
1205     }
1206 #endif
1207 
1208 #ifdef Q_OS_WIN
1209     // Slashes and backslashes are not allowed in Windows filenames; show error
1210     else if (text.contains(QLatin1Char('/'))) {
1211         m_messageWidget->setText(i18n("Slashes cannot be used in file and folder names."));
1212         m_messageWidget->setMessageType(KMessageWidget::Error);
1213         m_messageWidget->animatedShow();
1214         m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1215     } else if (text.contains(QLatin1Char('\\'))) {
1216         m_messageWidget->setText(i18n("Backslashes cannot be used in file and folder names."));
1217         m_messageWidget->setMessageType(KMessageWidget::Error);
1218         m_messageWidget->animatedShow();
1219         m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1220     }
1221 #endif
1222 
1223     // Using a tilde to begin a file or folder name is not recommended
1224     else if (text.startsWith(QLatin1Char('~'))) {
1225         m_messageWidget->setText(
1226             i18n("Starting a file or folder name with a tilde is not recommended because it may be confusing or dangerous when using the terminal to delete "
1227                  "things."));
1228         m_messageWidget->setMessageType(KMessageWidget::Warning);
1229         m_messageWidget->animatedShow();
1230     } else {
1231         m_messageWidget->hide();
1232     }
1233 
1234     if (!text.isEmpty()) {
1235         // Check file does not already exists
1236         m_statRunning = true;
1237         QUrl url;
1238         if (m_creatingDirectory && text.at(0) == QLatin1Char('~')) {
1239             url = QUrl::fromUserInput(KShell::tildeExpand(text));
1240         } else {
1241             url = QUrl(m_baseUrl.toString() + QLatin1Char('/') + text);
1242         }
1243         KIO::StatJob *job = KIO::statDetails(url, KIO::StatJob::StatSide::DestinationSide, KIO::StatDetail::StatBasic, KIO::HideProgressInfo);
1244         QObject::connect(job, &KJob::result, m_fileDialog, [this](KJob *job) {
1245             _k_slotStatResult(job);
1246         });
1247         job->start();
1248     }
1249 
1250     m_text = text;
1251 }
1252 
1253 void KNewFileMenu::setSelectDirWhenAlreadyExist(bool shouldSelectExistingDir)
1254 {
1255     d->m_selectDirWhenAlreadyExists = shouldSelectExistingDir;
1256 }
1257 
1258 void KNewFileMenuPrivate::_k_slotStatResult(KJob *job)
1259 {
1260     m_statRunning = false;
1261     KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
1262     // ignore stat Result when the lineEdit has changed
1263     const QUrl url = statJob->url().adjusted(QUrl::StripTrailingSlash);
1264     if (m_creatingDirectory && m_lineEdit->text().startsWith(QLatin1Char('~'))) {
1265         if (url.path() != KShell::tildeExpand(m_lineEdit->text())) {
1266             return;
1267         }
1268     } else if (url.fileName() != m_lineEdit->text()) {
1269         return;
1270     }
1271     bool accepted = m_acceptedPressed;
1272     m_acceptedPressed = false;
1273     auto error = job->error();
1274     if (error) {
1275         if (error == KIO::ERR_DOES_NOT_EXIST) {
1276             // fine for file creation
1277             if (accepted) {
1278                 m_fileDialog->accept();
1279             }
1280         } else {
1281             qWarning() << error << job->errorString();
1282         }
1283     } else {
1284         bool shouldEnable = false;
1285         KMessageWidget::MessageType messageType = KMessageWidget::Error;
1286 
1287         const KIO::UDSEntry &entry = statJob->statResult();
1288         if (entry.isDir()) {
1289             if (m_selectDirWhenAlreadyExists && m_creatingDirectory) {
1290                 // allow "overwrite" of dir
1291                 messageType = KMessageWidget::Information;
1292                 shouldEnable = true;
1293             }
1294             m_messageWidget->setText(xi18nc("@info", "A directory with name <filename>%1</filename> already exists.", m_text));
1295         } else {
1296             m_messageWidget->setText(xi18nc("@info", "A file with name <filename>%1</filename> already exists.", m_text));
1297         }
1298         m_messageWidget->setMessageType(messageType);
1299         m_messageWidget->animatedShow();
1300         m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(shouldEnable);
1301 
1302         if (accepted && shouldEnable) {
1303             m_fileDialog->accept();
1304         }
1305     }
1306 }
1307 
1308 void KNewFileMenuPrivate::slotUrlDesktopFile()
1309 {
1310     KNameAndUrlInputDialog *dlg = static_cast<KNameAndUrlInputDialog *>(m_fileDialog);
1311     QString name = dlg->name();
1312     const QLatin1String ext(".desktop");
1313     if (!name.endsWith(ext)) {
1314         name += ext;
1315     }
1316     m_copyData.m_chosenFileName = name; // no path
1317     QUrl linkUrl = dlg->url();
1318 
1319     // Filter user input so that short uri entries, e.g. www.kde.org, are
1320     // handled properly. This not only makes the icon detection below work
1321     // properly, but opening the URL link where the short uri will not be
1322     // sent to the application (opening such link Konqueror fails).
1323     KUriFilterData uriData;
1324     uriData.setData(linkUrl); // the url to put in the file
1325     uriData.setCheckForExecutables(false);
1326 
1327     if (KUriFilter::self()->filterUri(uriData, QStringList{QStringLiteral("kshorturifilter")})) {
1328         linkUrl = uriData.uri();
1329     }
1330 
1331     if (m_copyData.m_chosenFileName.isEmpty() || linkUrl.isEmpty()) {
1332         return;
1333     }
1334 
1335     // It's a "URL" desktop file; we need to make a temp copy of it, to modify it
1336     // before copying it to the final destination [which could be a remote protocol]
1337     QTemporaryFile tmpFile;
1338     tmpFile.setAutoRemove(false); // done below
1339     if (!tmpFile.open()) {
1340         qCritical() << "Couldn't create temp file!";
1341         return;
1342     }
1343 
1344     if (!checkSourceExists(m_copyData.m_templatePath)) {
1345         return;
1346     }
1347 
1348     // First copy the template into the temp file
1349     QFile file(m_copyData.m_templatePath);
1350     if (!file.open(QIODevice::ReadOnly)) {
1351         qCritical() << "Couldn't open template" << m_copyData.m_templatePath;
1352         return;
1353     }
1354     const QByteArray data = file.readAll();
1355     tmpFile.write(data);
1356     const QString tempFileName = tmpFile.fileName();
1357     Q_ASSERT(!tempFileName.isEmpty());
1358     tmpFile.close();
1359     file.close();
1360 
1361     KDesktopFile df(tempFileName);
1362     KConfigGroup group = df.desktopGroup();
1363 
1364     if (linkUrl.isLocalFile()) {
1365         KFileItem fi(linkUrl);
1366         group.writeEntry("Icon", fi.iconName());
1367     } else {
1368         group.writeEntry("Icon", KProtocolInfo::icon(linkUrl.scheme()));
1369     }
1370 
1371     group.writePathEntry("URL", linkUrl.toDisplayString());
1372     group.writeEntry("Name", dlg->name()); // Used as user-visible name by kio_desktop
1373     df.sync();
1374 
1375     m_copyData.m_src = tempFileName;
1376     m_copyData.m_tempFileToDelete = tempFileName;
1377 
1378     executeStrategy();
1379 }
1380 
1381 #if KIOFILEWIDGETS_BUILD_DEPRECATED_SINCE(5, 100)
1382 KNewFileMenu::KNewFileMenu(KActionCollection *collection, const QString &name, QObject *parent)
1383     : KActionMenu(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Create New"), parent)
1384     , d(std::make_unique<KNewFileMenuPrivate>(this))
1385 {
1386     // Don't fill the menu yet
1387     // We'll do that in checkUpToDate (should be connected to aboutToShow)
1388     d->m_newMenuGroup = new QActionGroup(this);
1389     connect(d->m_newMenuGroup, &QActionGroup::triggered, this, [this](QAction *action) {
1390         d->slotActionTriggered(action);
1391     });
1392     d->m_parentWidget = qobject_cast<QWidget *>(parent);
1393     d->m_newDirAction = nullptr;
1394 
1395     if (collection) {
1396         collection->addAction(name, this);
1397         d->m_actionCollection = collection;
1398     }
1399 
1400     d->m_menuDev = new KActionMenu(QIcon::fromTheme(QStringLiteral("drive-removable-media")), i18n("Link to Device"), this);
1401 }
1402 #endif
1403 
1404 KNewFileMenu::KNewFileMenu(QObject *parent)
1405     : KActionMenu(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Create New"), parent)
1406     , d(std::make_unique<KNewFileMenuPrivate>(this))
1407 {
1408     // Don't fill the menu yet
1409     // We'll do that in checkUpToDate (should be connected to aboutToShow)
1410     d->m_newMenuGroup = new QActionGroup(this);
1411     connect(d->m_newMenuGroup, &QActionGroup::triggered, this, [this](QAction *action) {
1412         d->slotActionTriggered(action);
1413     });
1414     d->m_parentWidget = qobject_cast<QWidget *>(parent);
1415     d->m_newDirAction = nullptr;
1416 
1417     d->m_menuDev = new KActionMenu(QIcon::fromTheme(QStringLiteral("drive-removable-media")), i18n("Link to Device"), this);
1418 }
1419 
1420 KNewFileMenu::~KNewFileMenu() = default;
1421 
1422 void KNewFileMenu::checkUpToDate()
1423 {
1424     KNewFileMenuSingleton *s = kNewMenuGlobals();
1425     // qDebug() << this << "m_menuItemsVersion=" << d->m_menuItemsVersion
1426     //              << "s->templatesVersion=" << s->templatesVersion;
1427     if (d->m_menuItemsVersion < s->templatesVersion || s->templatesVersion == 0) {
1428         // qDebug() << "recreating actions";
1429         // We need to clean up the action collection
1430         // We look for our actions using the group
1431         qDeleteAll(d->m_newMenuGroup->actions());
1432 
1433         if (!s->templatesList) { // No templates list up to now
1434             s->templatesList = new KNewFileMenuSingleton::EntryList;
1435             d->slotFillTemplates();
1436             s->parseFiles();
1437         }
1438 
1439         // This might have been already done for other popupmenus,
1440         // that's the point in s->filesParsed.
1441         if (!s->filesParsed) {
1442             s->parseFiles();
1443         }
1444 
1445         d->fillMenu();
1446 
1447         d->m_menuItemsVersion = s->templatesVersion;
1448     }
1449 }
1450 
1451 void KNewFileMenu::createDirectory()
1452 {
1453     if (d->m_popupFiles.isEmpty()) {
1454         return;
1455     }
1456 
1457     QString name = !d->m_text.isEmpty() ? d->m_text : i18nc("Default name for a new folder", "New Folder");
1458 
1459     d->m_baseUrl = d->m_popupFiles.first();
1460 
1461     auto nameJob = new KIO::NameFinderJob(d->m_baseUrl, name, this);
1462     connect(nameJob, &KJob::result, this, [=]() mutable {
1463         if (!nameJob->error()) {
1464             d->m_baseUrl = nameJob->baseUrl();
1465             name = nameJob->finalName();
1466         }
1467         d->showNewDirNameDlg(name);
1468     });
1469     nameJob->start();
1470 }
1471 
1472 void KNewFileMenuPrivate::showNewDirNameDlg(const QString &name)
1473 {
1474     initDialog();
1475 
1476     m_fileDialog->setWindowTitle(i18nc("@title:window", "New Folder"));
1477 
1478     m_label->setText(i18n("Create new folder in %1:", m_baseUrl.toDisplayString(QUrl::PreferLocalFile)));
1479 
1480     m_lineEdit->setText(name);
1481 
1482     m_creatingDirectory = true;
1483     _k_slotTextChanged(name); // have to save string in m_text in case user does not touch dialog
1484     QObject::connect(m_lineEdit, &QLineEdit::textChanged, q, [this]() {
1485         _k_delayedSlotTextChanged();
1486     });
1487     m_delayedSlotTextChangedTimer->callOnTimeout(m_lineEdit, [this]() {
1488         _k_slotTextChanged(m_lineEdit->text());
1489     });
1490 
1491     QObject::connect(m_fileDialog, &QDialog::accepted, q, [this]() {
1492         slotCreateDirectory();
1493     });
1494     QObject::connect(m_fileDialog, &QDialog::rejected, q, [this]() {
1495         slotAbortDialog();
1496     });
1497 
1498     m_fileDialog->show();
1499     m_lineEdit->selectAll();
1500     m_lineEdit->setFocus();
1501 }
1502 
1503 void KNewFileMenu::createFile()
1504 {
1505     if (d->m_popupFiles.isEmpty()) {
1506         return;
1507     }
1508 
1509     checkUpToDate();
1510     if (!d->m_firstFileEntry) {
1511         return;
1512     }
1513 
1514     d->executeRealFileOrDir(*d->m_firstFileEntry);
1515 }
1516 
1517 bool KNewFileMenu::isModal() const
1518 {
1519     return d->m_modal;
1520 }
1521 
1522 #if KIOFILEWIDGETS_BUILD_DEPRECATED_SINCE(5, 97)
1523 QList<QUrl> KNewFileMenu::popupFiles() const
1524 {
1525     return d->m_popupFiles;
1526 }
1527 #endif
1528 
1529 void KNewFileMenu::setModal(bool modal)
1530 {
1531     d->m_modal = modal;
1532 }
1533 
1534 #if KIOFILEWIDGETS_BUILD_DEPRECATED_SINCE(5, 97)
1535 void KNewFileMenu::setPopupFiles(const QList<QUrl> &files)
1536 {
1537     d->m_popupFiles = files;
1538     if (files.isEmpty()) {
1539         d->m_newMenuGroup->setEnabled(false);
1540     } else {
1541         const QUrl &firstUrl = files.first();
1542         if (KProtocolManager::supportsWriting(firstUrl)) {
1543             d->m_newMenuGroup->setEnabled(true);
1544             if (d->m_newDirAction) {
1545                 d->m_newDirAction->setEnabled(KProtocolManager::supportsMakeDir(firstUrl)); // e.g. trash:/
1546             }
1547         } else {
1548             d->m_newMenuGroup->setEnabled(true);
1549         }
1550     }
1551 }
1552 #endif
1553 
1554 void KNewFileMenu::setParentWidget(QWidget *parentWidget)
1555 {
1556     d->m_parentWidget = parentWidget;
1557 }
1558 
1559 void KNewFileMenu::setSupportedMimeTypes(const QStringList &mime)
1560 {
1561     d->m_supportedMimeTypes = mime;
1562 }
1563 
1564 #if KIOFILEWIDGETS_BUILD_DEPRECATED_SINCE(5, 97)
1565 void KNewFileMenu::setViewShowsHiddenFiles(bool b)
1566 {
1567     Q_UNUSED(b)
1568 }
1569 #endif
1570 
1571 void KNewFileMenu::slotResult(KJob *job)
1572 {
1573     if (job->error()) {
1574         if (job->error() == KIO::ERR_DIR_ALREADY_EXIST) {
1575             auto *simpleJob = ::qobject_cast<KIO::SimpleJob *>(job);
1576             if (simpleJob) {
1577                 Q_ASSERT(d->m_selectDirWhenAlreadyExists);
1578                 const QUrl jobUrl = simpleJob->url();
1579                 // Select the existing dir
1580                 Q_EMIT selectExistingDir(jobUrl);
1581             }
1582         } else { // All other errors
1583             static_cast<KIO::Job *>(job)->uiDelegate()->showErrorMessage();
1584         }
1585     } else {
1586         // Was this a copy or a mkdir?
1587         if (job->property("newDirectoryURL").isValid()) {
1588             QUrl newDirectoryURL = job->property("newDirectoryURL").toUrl();
1589             Q_EMIT directoryCreated(newDirectoryURL);
1590         } else {
1591             KIO::CopyJob *copyJob = ::qobject_cast<KIO::CopyJob *>(job);
1592             if (copyJob) {
1593                 const QUrl destUrl = copyJob->destUrl();
1594                 const QUrl localUrl = d->mostLocalUrl(destUrl);
1595                 if (localUrl.isLocalFile()) {
1596                     // Normal (local) file. Need to "touch" it, kio_file copied the mtime.
1597                     (void)::utime(QFile::encodeName(localUrl.toLocalFile()).constData(), nullptr);
1598                 }
1599                 Q_EMIT fileCreated(destUrl);
1600             } else if (KIO::SimpleJob *simpleJob = ::qobject_cast<KIO::SimpleJob *>(job)) {
1601                 // Called in the storedPut() case
1602                 org::kde::KDirNotify::emitFilesAdded(simpleJob->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
1603                 Q_EMIT fileCreated(simpleJob->url());
1604             }
1605         }
1606     }
1607     if (!d->m_tempFileToDelete.isEmpty()) {
1608         QFile::remove(d->m_tempFileToDelete);
1609     }
1610 }
1611 
1612 QStringList KNewFileMenu::supportedMimeTypes() const
1613 {
1614     return d->m_supportedMimeTypes;
1615 }
1616 
1617 void KNewFileMenu::setWorkingDirectory(const QUrl &directory)
1618 {
1619     d->m_popupFiles = {directory};
1620 
1621     if (directory.isEmpty()) {
1622         d->m_newMenuGroup->setEnabled(false);
1623     } else {
1624         if (KProtocolManager::supportsWriting(directory)) {
1625             d->m_newMenuGroup->setEnabled(true);
1626             if (d->m_newDirAction) {
1627                 d->m_newDirAction->setEnabled(KProtocolManager::supportsMakeDir(directory)); // e.g. trash:/
1628             }
1629         } else {
1630             d->m_newMenuGroup->setEnabled(true);
1631         }
1632     }
1633 }
1634 
1635 QUrl KNewFileMenu::workingDirectory() const
1636 {
1637     return d->m_popupFiles.isEmpty() ? QUrl() : d->m_popupFiles.first();
1638 }
1639 
1640 void KNewFileMenu::setNewFolderShortcutAction(QAction *action)
1641 {
1642     d->m_newFolderShortcutAction = action;
1643 }
1644 
1645 void KNewFileMenu::setNewFileShortcutAction(QAction *action)
1646 {
1647     d->m_newFileShortcutAction = action;
1648 }
1649 
1650 #include "moc_knewfilemenu.cpp"