File indexing completed on 2024-04-14 03:53:13

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