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