File indexing completed on 2024-09-15 03:38:44
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"