KIO

knewfilemenu.cpp
1/*
2 This file is part of the KDE project
3 SPDX-FileCopyrightText: 1998-2009 David Faure <faure@kde.org>
4 SPDX-FileCopyrightText: 2003 Sven Leiber <s.leiber@web.de>
5
6 SPDX-License-Identifier: LGPL-2.0-only OR LGPL-3.0-only
7*/
8
9#include "knewfilemenu.h"
10#include "../utils_p.h"
11#include "kfilewidgets_debug.h"
12#include "knameandurlinputdialog.h"
13
14#include <kdirnotify.h>
15#include <kio/copyjob.h>
16#include <kio/fileundomanager.h>
17#include <kio/jobuidelegate.h>
18#include <kio/mkdirjob.h>
19#include <kio/mkpathjob.h>
20#include <kio/namefinderjob.h>
21#include <kio/statjob.h>
22#include <kio/storedtransferjob.h>
23#include <kpropertiesdialog.h>
24#include <kprotocolinfo.h>
25#include <kprotocolmanager.h>
26#include <kurifilter.h>
27
28#include <KConfigGroup>
29#include <KDesktopFile>
30#include <KDirOperator>
31#include <KDirWatch>
32#include <KFileUtils>
33#include <KJobWidgets>
34#include <KLocalizedString>
35#include <KMessageBox>
36#include <KMessageWidget>
37#include <KShell>
38
39#include <QActionGroup>
40#include <QDebug>
41#include <QDialog>
42#include <QDialogButtonBox>
43#include <QDir>
44#include <QLabel>
45#include <QLineEdit>
46#include <QList>
47#include <QLoggingCategory>
48#include <QMenu>
49#include <QMimeDatabase>
50#include <QPushButton>
51#include <QStandardPaths>
52#include <QTemporaryFile>
53#include <QTimer>
54#include <QVBoxLayout>
55
56#ifdef Q_OS_WIN
57#include <sys/utime.h>
58#else
59#include <utime.h>
60#endif
61
62#include <set>
63
64static QString expandTilde(const QString &name, bool isfile = false)
65{
66 if (name.isEmpty() || name == QLatin1Char('~')) {
67 return name;
68 }
69
70 QString expandedName;
71 if (!isfile || name[0] == QLatin1Char('\\')) {
72 expandedName = KShell::tildeExpand(name);
73 }
74
75 // If a tilde mark cannot be properly expanded, KShell::tildeExpand returns an empty string
76 return !expandedName.isEmpty() ? expandedName : name;
77}
78
79// Singleton, with data shared by all KNewFileMenu instances
80class KNewFileMenuSingleton
81{
82public:
83 KNewFileMenuSingleton()
84 : dirWatch(nullptr)
85 , filesParsed(false)
86 , templatesList(nullptr)
87 , templatesVersion(0)
88 {
89 }
90
91 ~KNewFileMenuSingleton()
92 {
93 delete templatesList;
94 }
95
96 /**
97 * Opens the desktop files and completes the Entry list
98 * Input: the entry list. Output: the entry list ;-)
99 */
100 void parseFiles();
101
102 enum EntryType {
103 Unknown = 0, // Not parsed, i.e. we don't know
104 LinkToTemplate, // A desktop file that points to a file or dir to copy
105 Template, // A real file to copy as is (the KDE-1.x solution)
106 };
107
108 std::unique_ptr<KDirWatch> dirWatch;
109
110 struct Entry {
111 QString text;
112 QString filePath; /// The displayed name in the context menu and the suggested filename. When using a .desktop file this is used to refer back to
113 /// it during parsing.
114 QString templatePath; /// Where the file is copied from, the suggested file extension and whether the menu entries have a separator around them.
115 /// Same as filePath for Template.
116 QString icon; /// The icon displayed in the context menu
117 EntryType entryType; /// Defines if the created file will be a copy or a symbolic link
118 QString comment; /// The prompt label asking for filename
119 QString mimeType;
120 };
121 // NOTE: only filePath is known before we call parseFiles
122
123 /**
124 * List of all template files. It is important that they are in
125 * the same order as the 'New' menu.
126 */
127 typedef QList<Entry> EntryList;
128
129 /**
130 * Set back to false each time new templates are found,
131 * and to true on the first call to parseFiles
132 */
133 bool filesParsed;
134 EntryList *templatesList;
135
136 /**
137 * Is increased when templatesList has been updated and
138 * menu needs to be re-filled. Menus have their own version and compare it
139 * to templatesVersion before showing up
140 */
141 int templatesVersion;
142};
143
144struct EntryInfo {
145 QString key; /// Context menu order is the alphabetical order of this variable
146 QString url;
147 KNewFileMenuSingleton::Entry entry;
148};
149
150void KNewFileMenuSingleton::parseFiles()
151{
152 // qDebug();
153 filesParsed = true;
154 QMutableListIterator templIter(*templatesList);
155 while (templIter.hasNext()) {
156 KNewFileMenuSingleton::Entry &templ = templIter.next();
157 const QString &filePath = templ.filePath;
158 QString text;
159 QString templatePath;
160 // If a desktop file, then read the name from it.
161 // Otherwise (or if no name in it?) use file name
162 if (KDesktopFile::isDesktopFile(filePath)) {
163 KDesktopFile desktopFile(filePath);
164 if (desktopFile.noDisplay()) {
165 templIter.remove();
166 continue;
167 }
168
169 text = desktopFile.readName();
170 templ.icon = desktopFile.readIcon();
171 templ.comment = desktopFile.readComment();
172 if (desktopFile.readType() == QLatin1String("Link")) {
173 templatePath = desktopFile.desktopGroup().readPathEntry("URL", QString());
174 if (templatePath.startsWith(QLatin1String("file:/"))) {
175 templatePath = QUrl(templatePath).toLocalFile();
176 } else if (!templatePath.startsWith(QLatin1Char('/')) && !templatePath.startsWith(QLatin1String("__"))) {
177 // A relative path, then (that's the default in the files we ship)
178 const QStringView linkDir = QStringView(filePath).left(filePath.lastIndexOf(QLatin1Char('/')) + 1 /*keep / */);
179 // qDebug() << "linkDir=" << linkDir;
180 templatePath = linkDir + templatePath;
181 }
182 }
183 if (templatePath.isEmpty()) {
184 // No URL key, this is an old-style template
185 templ.entryType = KNewFileMenuSingleton::Template;
186 templ.templatePath = templ.filePath; // we'll copy the file
187 } else {
188 templ.entryType = KNewFileMenuSingleton::LinkToTemplate;
189 templ.templatePath = templatePath;
190 }
191 }
192 if (text.isEmpty()) {
193 text = QUrl(filePath).fileName();
194 const QLatin1String suffix(".desktop");
195 if (text.endsWith(suffix)) {
196 text.chop(suffix.size());
197 }
198 }
199 templ.text = text;
200 /*// qDebug() << "Updating entry with text=" << text
201 << "entryType=" << templ.entryType
202 << "templatePath=" << templ.templatePath;*/
203 }
204}
205
206Q_GLOBAL_STATIC(KNewFileMenuSingleton, kNewMenuGlobals)
207
208class KNewFileMenuCopyData
209{
210public:
211 KNewFileMenuCopyData()
212 {
213 m_isSymlink = false;
214 }
215 QString chosenFileName() const
216 {
217 return m_chosenFileName;
218 }
219
220 // If empty, no copy is performed.
221 QString sourceFileToCopy() const
222 {
223 return m_src;
224 }
225 QString tempFileToDelete() const
226 {
227 return m_tempFileToDelete;
228 }
229 bool m_isSymlink;
230
231 QString m_chosenFileName;
232 QString m_src;
233 QString m_tempFileToDelete;
234 QString m_templatePath;
235};
236
237class KNewFileMenuPrivate
238{
239public:
240 explicit KNewFileMenuPrivate(KNewFileMenu *qq)
241 : q(qq)
242 , m_delayedSlotTextChangedTimer(new QTimer(q))
243 {
244 m_delayedSlotTextChangedTimer->setInterval(50);
245 m_delayedSlotTextChangedTimer->setSingleShot(true);
246 }
247
248 bool checkSourceExists(const QString &src);
249
250 /**
251 * The strategy used for other desktop files than Type=Link. Example: Application, Device.
252 */
253 void executeOtherDesktopFile(const KNewFileMenuSingleton::Entry &entry);
254
255 /**
256 * The strategy used for "real files or directories" (the common case)
257 */
258 void executeRealFileOrDir(const KNewFileMenuSingleton::Entry &entry);
259
260 /**
261 * Actually performs file handling. Reads in m_copyData for needed data, that has been collected by execute*() before
262 */
263 void executeStrategy();
264
265 /**
266 * The strategy used when creating a symlink
267 */
268 void executeSymLink(const KNewFileMenuSingleton::Entry &entry);
269
270 /**
271 * The strategy used for "url" desktop files
272 */
273 void executeUrlDesktopFile(const KNewFileMenuSingleton::Entry &entry);
274
275 /**
276 * Fills the menu from the templates list.
277 */
278 void fillMenu();
279
280 /**
281 * Tries to map a local URL for the given URL.
282 */
283 QUrl mostLocalUrl(const QUrl &url);
284
285 /**
286 * Just clears the string buffer d->m_text, but I need a slot for this to occur
287 */
288 void slotAbortDialog();
289
290 /**
291 * Called when New->* is clicked
292 */
293 void slotActionTriggered(QAction *action);
294
295 /**
296 * Shows a dialog asking the user to enter a name when creating a new folder.
297 */
298 void showNewDirNameDlg(const QString &name);
299
300 /**
301 * Callback function that reads in directory name from dialog and processes it
302 */
303 void slotCreateDirectory();
304
305 /**
306 * Fills the templates list.
307 */
308 void slotFillTemplates();
309
310 /**
311 * Called when accepting the KPropertiesDialog (for "other desktop files")
312 */
313 void _k_slotOtherDesktopFile(KPropertiesDialog *sender);
314
315 /**
316 * Called when closing the KPropertiesDialog is closed (whichever way, accepted and rejected)
317 */
318 void slotOtherDesktopFileClosed();
319
320 /**
321 * Callback in KNewFileMenu for the RealFile Dialog. Handles dialog input and gives over
322 * to executeStrategy()
323 */
324 void slotRealFileOrDir();
325
326 /**
327 * Delay calls to _k_slotTextChanged
328 */
329 void _k_delayedSlotTextChanged();
330
331 /**
332 * Dialogs use this slot to write the changed string into KNewFile menu when the user
333 * changes touches them
334 */
335 void _k_slotTextChanged(const QString &text);
336
337 /**
338 * Callback in KNewFileMenu for the Symlink Dialog. Handles dialog input and gives over
339 * to executeStrategy()
340 */
341 void slotSymLink();
342
343 /**
344 * Callback in KNewFileMenu for the Url/Desktop Dialog. Handles dialog input and gives over
345 * to executeStrategy()
346 */
347 void slotUrlDesktopFile();
348
349 /**
350 * Callback to check if a file/directory with the same name as the one being created, exists
351 */
352 void _k_slotStatResult(KJob *job);
353
354 void _k_slotAccepted();
355
356 /**
357 * Initializes m_fileDialog and the other widgets that are included in it. Mainly to reduce
358 * code duplication in showNewDirNameDlg() and executeRealFileOrDir().
359 */
360 void initDialog();
361
362 QAction *m_newFolderShortcutAction = nullptr;
363 QAction *m_newFileShortcutAction = nullptr;
364
365 KActionMenu *m_menuDev = nullptr;
366 int m_menuItemsVersion = 0;
367 QAction *m_newDirAction = nullptr;
368 QDialog *m_fileDialog = nullptr;
369 KMessageWidget *m_messageWidget = nullptr;
370 QLabel *m_label = nullptr;
371 QLineEdit *m_lineEdit = nullptr;
372 QDialogButtonBox *m_buttonBox = nullptr;
373
374 // This is used to allow _k_slotTextChanged to know whether it's being used to
375 // create a file or a directory without duplicating code across two functions
376 bool m_creatingDirectory = false;
377 bool m_modal = true;
378
379 /**
380 * The action group that our actions belong to
381 */
382 QActionGroup *m_newMenuGroup = nullptr;
383 QWidget *m_parentWidget = nullptr;
384
385 /**
386 * When the user pressed the right mouse button over an URL a popup menu
387 * is displayed. The URL belonging to this popup menu is stored here.
388 * For all intents and purposes this is the current directory where the menu is
389 * opened.
390 * TODO KF6 make it a single QUrl.
391 */
392 QList<QUrl> m_popupFiles;
393
394 QStringList m_supportedMimeTypes;
395 QString m_tempFileToDelete; // set when a tempfile was created for a Type=URL desktop file
396 QString m_text;
397 QString m_windowTitle;
398
399 KNewFileMenuSingleton::Entry *m_firstFileEntry = nullptr;
400
401 KNewFileMenu *const q;
402
403 KNewFileMenuCopyData m_copyData;
404
405 /**
406 * Use to delay a bit feedback to user
407 */
408 QTimer *m_delayedSlotTextChangedTimer;
409
410 QUrl m_baseUrl;
411
412 bool m_selectDirWhenAlreadyExists = false;
413 bool m_acceptedPressed = false;
414 bool m_statRunning = false;
415 bool m_isCreateDirectoryRunning = false;
416 bool m_isCreateFileRunning = false;
417};
418
419void KNewFileMenuPrivate::_k_slotAccepted()
420{
421 if (m_statRunning || m_delayedSlotTextChangedTimer->isActive()) {
422 // stat is running or _k_slotTextChanged has not been called already
423 // delay accept until stat has been run
424 m_acceptedPressed = true;
425
426 if (m_delayedSlotTextChangedTimer->isActive()) {
427 m_delayedSlotTextChangedTimer->stop();
428 _k_slotTextChanged(m_lineEdit->text());
429 }
430 } else {
431 m_fileDialog->accept();
432 }
433}
434
435void KNewFileMenuPrivate::initDialog()
436{
437 m_fileDialog = new QDialog(m_parentWidget);
438 m_fileDialog->setAttribute(Qt::WA_DeleteOnClose);
439 m_fileDialog->setModal(m_modal);
440 m_fileDialog->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum);
441 m_fileDialog->setWindowTitle(m_windowTitle.isEmpty() ? i18nc("@title:window", "Create New File") : m_windowTitle);
442
443 m_messageWidget = new KMessageWidget(m_fileDialog);
444 m_messageWidget->setCloseButtonVisible(false);
445 m_messageWidget->setWordWrap(true);
446 m_messageWidget->hide();
447
448 m_label = new QLabel(m_fileDialog);
449
450 m_lineEdit = new QLineEdit(m_fileDialog);
451 m_lineEdit->setClearButtonEnabled(true);
452 m_lineEdit->setMinimumWidth(400);
453
454 m_buttonBox = new QDialogButtonBox(m_fileDialog);
455 m_buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
456 QObject::connect(m_buttonBox, &QDialogButtonBox::accepted, [this]() {
457 _k_slotAccepted();
458 });
459 QObject::connect(m_buttonBox, &QDialogButtonBox::rejected, m_fileDialog, &QDialog::reject);
460
461 QObject::connect(m_fileDialog, &QDialog::finished, m_fileDialog, [this] {
462 m_statRunning = false;
463 });
464
465 QVBoxLayout *layout = new QVBoxLayout(m_fileDialog);
466 layout->setSizeConstraint(QLayout::SetFixedSize);
467
468 layout->addWidget(m_label);
469 layout->addWidget(m_lineEdit);
470 layout->addWidget(m_buttonBox);
471 layout->addWidget(m_messageWidget);
472 layout->addStretch();
473}
474
475bool KNewFileMenuPrivate::checkSourceExists(const QString &src)
476{
477 if (!QFile::exists(src)) {
478 qWarning() << src << "doesn't exist";
479
480 QDialog *dialog = new QDialog(m_parentWidget);
481 dialog->setWindowTitle(i18n("Sorry"));
482 dialog->setObjectName(QStringLiteral("sorry"));
483 dialog->setModal(q->isModal());
485
486 QDialogButtonBox *box = new QDialogButtonBox(dialog);
488
490 box,
492 i18n("<qt>The template file <b>%1</b> does not exist.</qt>", src),
493 QStringList(),
494 QString(),
495 nullptr,
497
498 dialog->show();
499
500 return false;
501 }
502 return true;
503}
504
505void KNewFileMenuPrivate::executeOtherDesktopFile(const KNewFileMenuSingleton::Entry &entry)
506{
507 if (!checkSourceExists(entry.templatePath)) {
508 return;
509 }
510
511 for (const auto &url : std::as_const(m_popupFiles)) {
512 QString text = entry.text;
513 text.remove(QStringLiteral("...")); // the ... is fine for the menu item but not for the default filename
514 text = text.trimmed(); // In some languages, there is a space in front of "...", see bug 268895
515 // KDE5 TODO: remove the "..." from link*.desktop files and use i18n("%1...") when making
516 // the action.
517 QString name = text;
518 text.append(QStringLiteral(".desktop"));
519
520 const QUrl directory = mostLocalUrl(url);
521 const QUrl defaultFile = QUrl::fromLocalFile(directory.toLocalFile() + QLatin1Char('/') + KIO::encodeFileName(text));
522 if (defaultFile.isLocalFile() && QFile::exists(defaultFile.toLocalFile())) {
523 text = KFileUtils::suggestName(directory, text);
524 }
525
526 QUrl templateUrl;
527 bool usingTemplate = false;
528 if (entry.templatePath.startsWith(QLatin1String(":/"))) {
529 QTemporaryFile *tmpFile = QTemporaryFile::createNativeFile(entry.templatePath);
530 tmpFile->setAutoRemove(false);
531 QString tempFileName = tmpFile->fileName();
532 tmpFile->close();
533
534 KDesktopFile df(tempFileName);
535 KConfigGroup group = df.desktopGroup();
536 group.writeEntry("Name", name);
537 templateUrl = QUrl::fromLocalFile(tempFileName);
538 m_tempFileToDelete = tempFileName;
539 usingTemplate = true;
540 } else {
541 templateUrl = QUrl::fromLocalFile(entry.templatePath);
542 }
543 KPropertiesDialog *dlg = new KPropertiesDialog(templateUrl, directory, text, m_parentWidget);
544 dlg->setModal(q->isModal());
546 QObject::connect(dlg, &KPropertiesDialog::applied, q, [this, dlg]() {
547 _k_slotOtherDesktopFile(dlg);
548 });
549 if (usingTemplate) {
551 slotOtherDesktopFileClosed();
552 });
553 }
554 dlg->show();
555 }
556 // We don't set m_src here -> there will be no copy, we are done.
557}
558
559void KNewFileMenuPrivate::executeRealFileOrDir(const KNewFileMenuSingleton::Entry &entry)
560{
561 Q_EMIT q->fileCreationStarted(QUrl(entry.filePath));
562
563 initDialog();
564
565 const auto getSelectionLength = [](const QString &text) {
566 // Select the text without MIME-type extension
567 int selectionLength = text.length();
568
569 QMimeDatabase db;
570 const QString extension = db.suffixForFileName(text);
571 if (extension.isEmpty()) {
572 // For an unknown extension just exclude the extension after
573 // the last point. This does not work for multiple extensions like
574 // *.tar.gz but usually this is anyhow a known extension.
575 selectionLength = text.lastIndexOf(QLatin1Char('.'));
576
577 // If no point could be found, use whole text length for selection.
578 if (selectionLength < 1) {
579 selectionLength = text.length();
580 }
581
582 } else {
583 selectionLength -= extension.length() + 1;
584 }
585
586 return selectionLength;
587 };
588
589 // The template is not a desktop file
590 // Prompt the user to set the destination filename
591 QString text = entry.text;
592 text.remove(QStringLiteral("...")); // the ... is fine for the menu item but not for the default filename
593 text = text.trimmed(); // In some languages, there is a space in front of "...", see bug 268895
594 // add the extension (from the templatePath), should work with .txt, .html and with ".tar.gz"... etc
595 const QString fileName = entry.templatePath.mid(entry.templatePath.lastIndexOf(QLatin1Char('/')));
596 const int dotIndex = getSelectionLength(fileName);
597 text += dotIndex > 0 ? fileName.mid(dotIndex) : QString();
598
599 m_copyData.m_src = entry.templatePath;
600
601 const QUrl directory = mostLocalUrl(m_popupFiles.first());
602 m_baseUrl = directory;
603 const QUrl defaultFile = QUrl::fromLocalFile(directory.toLocalFile() + QLatin1Char('/') + KIO::encodeFileName(text));
604 if (defaultFile.isLocalFile() && QFile::exists(defaultFile.toLocalFile())) {
605 text = KFileUtils::suggestName(directory, text);
606 }
607
608 m_label->setText(entry.comment);
609
610 m_lineEdit->setText(text);
611
612 m_creatingDirectory = false;
613 _k_slotTextChanged(text);
614 QObject::connect(m_lineEdit, &QLineEdit::textChanged, q, [this]() {
615 _k_delayedSlotTextChanged();
616 });
617 m_delayedSlotTextChangedTimer->callOnTimeout(m_lineEdit, [this]() {
618 _k_slotTextChanged(m_lineEdit->text());
619 });
620
621 QObject::connect(m_fileDialog, &QDialog::accepted, q, [this]() {
622 slotRealFileOrDir();
623 });
624 QObject::connect(m_fileDialog, &QDialog::rejected, q, [this]() {
625 slotAbortDialog();
626 });
627
628 m_fileDialog->show();
629
630 const int firstDotInBaseName = getSelectionLength(text);
631 m_lineEdit->setSelection(0, firstDotInBaseName > 0 ? firstDotInBaseName : text.size());
632
633 m_lineEdit->setFocus();
634}
635
636void KNewFileMenuPrivate::executeSymLink(const KNewFileMenuSingleton::Entry &entry)
637{
638 KNameAndUrlInputDialog *dlg = new KNameAndUrlInputDialog(i18n("Name for new link:"), entry.comment, m_popupFiles.first(), m_parentWidget);
639 dlg->setModal(q->isModal());
641 dlg->setWindowTitle(i18n("Create Symlink"));
642 m_fileDialog = dlg;
643 QObject::connect(dlg, &QDialog::accepted, q, [this]() {
644 slotSymLink();
645 });
646 dlg->show();
647}
648
649void KNewFileMenuPrivate::executeStrategy()
650{
651 m_tempFileToDelete = m_copyData.tempFileToDelete();
652 const QString src = m_copyData.sourceFileToCopy();
653 QString chosenFileName = expandTilde(m_copyData.chosenFileName(), true);
654
655 if (src.isEmpty()) {
656 return;
657 }
658 QUrl uSrc(QUrl::fromLocalFile(src));
659
660 // In case the templates/.source directory contains symlinks, resolve
661 // them to the target files. Fixes bug #149628.
662 KFileItem item(uSrc, QString(), KFileItem::Unknown);
663 if (item.isLink()) {
664 uSrc.setPath(item.linkDest());
665 }
666
667 // The template is not a desktop file [or it's a URL one] >>> Copy it
668 for (const auto &u : std::as_const(m_popupFiles)) {
669 QUrl dest = u;
670 dest.setPath(Utils::concatPaths(dest.path(), KIO::encodeFileName(chosenFileName)));
671
672 QList<QUrl> lstSrc;
673 lstSrc.append(uSrc);
674 KIO::Job *kjob;
675 if (m_copyData.m_isSymlink) {
676 KIO::CopyJob *linkJob = KIO::linkAs(uSrc, dest);
677 kjob = linkJob;
679 } else if (src.startsWith(QLatin1String(":/"))) {
680 QFile srcFile(src);
681 if (!srcFile.open(QIODevice::ReadOnly)) {
682 return;
683 }
684 // The QFile won't live long enough for the job, so let's buffer the contents
685 const QByteArray srcBuf(srcFile.readAll());
686 KIO::StoredTransferJob *putJob = KIO::storedPut(srcBuf, dest, -1);
687 kjob = putJob;
688 KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Put, QList<QUrl>(), dest, putJob);
689 } else {
690 // qDebug() << "KIO::copyAs(" << uSrc.url() << "," << dest.url() << ")";
691 KIO::CopyJob *job = KIO::copyAs(uSrc, dest);
692 job->setDefaultPermissions(true);
693 kjob = job;
695 }
696 KJobWidgets::setWindow(kjob, m_parentWidget);
698 }
699}
700
701void KNewFileMenuPrivate::executeUrlDesktopFile(const KNewFileMenuSingleton::Entry &entry)
702{
703 KNameAndUrlInputDialog *dlg = new KNameAndUrlInputDialog(i18n("Name for new link:"), entry.comment, m_popupFiles.first(), m_parentWidget);
704 m_copyData.m_templatePath = entry.templatePath;
705 dlg->setModal(q->isModal());
707 dlg->setWindowTitle(i18n("Create link to URL"));
708 m_fileDialog = dlg;
709 QObject::connect(dlg, &QDialog::accepted, q, [this]() {
710 slotUrlDesktopFile();
711 });
712 dlg->show();
713}
714
715void KNewFileMenuPrivate::fillMenu()
716{
717 QMenu *menu = q->menu();
718 menu->clear();
719 m_menuDev->menu()->clear();
720 m_newDirAction = nullptr;
721
722 std::set<QString> seenTexts;
723 QString lastTemplatePath;
724 // these shall be put at special positions
725 QAction *linkURL = nullptr;
726 QAction *linkApp = nullptr;
727 QAction *linkPath = nullptr;
728
729 KNewFileMenuSingleton *s = kNewMenuGlobals();
730 int idx = 0;
731 for (auto &entry : *s->templatesList) {
732 ++idx;
733 if (entry.entryType != KNewFileMenuSingleton::Unknown) {
734 // There might be a .desktop for that one already.
735
736 // In fact, we skip any second item that has the same text as another one.
737 // Duplicates in a menu look bad in any case.
738 const auto [it, isInserted] = seenTexts.insert(entry.text);
739 if (isInserted) {
740 // const KNewFileMenuSingleton::Entry entry = templatesList->at(i-1);
741
742 const QString templatePath = entry.templatePath;
743 // The best way to identify the "Create Directory", "Link to Location", "Link to Application" was the template
744 if (templatePath.endsWith(QLatin1String("emptydir"))) {
745 QAction *act = new QAction(q);
746 m_newDirAction = act;
747 act->setIcon(QIcon::fromTheme(entry.icon));
748 act->setText(i18nc("@item:inmenu Create New", "%1", entry.text));
749 act->setActionGroup(m_newMenuGroup);
750
751 // If there is a shortcut action copy its shortcut
752 if (m_newFolderShortcutAction) {
753 act->setShortcuts(m_newFolderShortcutAction->shortcuts());
754 // Both actions have now the same shortcut, so this will prevent the "Ambiguous shortcut detected" dialog.
756 // We also need to react to shortcut changes.
757 QObject::connect(m_newFolderShortcutAction, &QAction::changed, act, [act, this]() {
758 act->setShortcuts(m_newFolderShortcutAction->shortcuts());
759 });
760 }
761
762 menu->addAction(act);
763 menu->addSeparator();
764 } else {
765 if (lastTemplatePath.startsWith(QDir::homePath()) && !templatePath.startsWith(QDir::homePath())) {
766 menu->addSeparator();
767 }
768 if (!m_supportedMimeTypes.isEmpty()) {
769 bool keep = false;
770
771 // We need to do MIME type filtering, for real files.
772 const bool createSymlink = entry.templatePath == QLatin1String("__CREATE_SYMLINK__");
773 if (createSymlink) {
774 keep = true;
775 } else if (!KDesktopFile::isDesktopFile(entry.templatePath)) {
776 // Determine MIME type on demand
777 QMimeDatabase db;
778 QMimeType mime;
779 if (entry.mimeType.isEmpty()) {
780 mime = db.mimeTypeForFile(entry.templatePath);
781 // qDebug() << entry.templatePath << "is" << mime.name();
782 entry.mimeType = mime.name();
783 } else {
784 mime = db.mimeTypeForName(entry.mimeType);
785 }
786 for (const QString &supportedMime : std::as_const(m_supportedMimeTypes)) {
787 if (mime.inherits(supportedMime)) {
788 keep = true;
789 break;
790 }
791 }
792 }
793
794 if (!keep) {
795 // qDebug() << "Not keeping" << entry.templatePath;
796 continue;
797 }
798 }
799
800 QAction *act = new QAction(q);
801 act->setData(idx);
802 act->setIcon(QIcon::fromTheme(entry.icon));
803 act->setText(i18nc("@item:inmenu Create New", "%1", entry.text));
804 act->setActionGroup(m_newMenuGroup);
805
806 // qDebug() << templatePath << entry.filePath;
807
808 if (templatePath.endsWith(QLatin1String("/URL.desktop"))) {
809 linkURL = act;
810 } else if (templatePath.endsWith(QLatin1String("/Program.desktop"))) {
811 linkApp = act;
812 } else if (entry.filePath.endsWith(QLatin1String("/linkPath.desktop"))) {
813 linkPath = act;
814 } else if (KDesktopFile::isDesktopFile(templatePath)) {
815 KDesktopFile df(templatePath);
816 if (df.readType() == QLatin1String("FSDevice")) {
817 m_menuDev->menu()->addAction(act);
818 } else {
819 menu->addAction(act);
820 }
821 } else {
822 if (!m_firstFileEntry) {
823 m_firstFileEntry = &entry;
824
825 // If there is a shortcut action copy its shortcut
826 if (m_newFileShortcutAction) {
827 act->setShortcuts(m_newFileShortcutAction->shortcuts());
828 // Both actions have now the same shortcut, so this will prevent the "Ambiguous shortcut detected" dialog.
830 // We also need to react to shortcut changes.
831 QObject::connect(m_newFileShortcutAction, &QAction::changed, act, [act, this]() {
832 act->setShortcuts(m_newFileShortcutAction->shortcuts());
833 });
834 }
835 }
836 menu->addAction(act);
837 }
838 }
839 }
840 lastTemplatePath = entry.templatePath;
841 } else { // Separate system from personal templates
842 Q_ASSERT(entry.entryType != 0);
843 menu->addSeparator();
844 }
845 }
846
847 if (m_supportedMimeTypes.isEmpty()) {
848 menu->addSeparator();
849 if (linkURL) {
850 menu->addAction(linkURL);
851 }
852 if (linkPath) {
853 menu->addAction(linkPath);
854 }
855 if (linkApp) {
856 menu->addAction(linkApp);
857 }
858 Q_ASSERT(m_menuDev);
859 if (!m_menuDev->menu()->isEmpty()) {
860 menu->addAction(m_menuDev);
861 }
862 }
863}
864
865QUrl KNewFileMenuPrivate::mostLocalUrl(const QUrl &url)
866{
867 if (url.isLocalFile() || KProtocolInfo::protocolClass(url.scheme()) != QLatin1String(":local")) {
868 return url;
869 }
870
871 KIO::StatJob *job = KIO::mostLocalUrl(url);
872 KJobWidgets::setWindow(job, m_parentWidget);
873
874 return job->exec() ? job->mostLocalUrl() : url;
875}
876
877void KNewFileMenuPrivate::slotAbortDialog()
878{
879 m_text = QString();
880 if (m_creatingDirectory) {
881 Q_EMIT q->directoryCreationRejected(m_baseUrl);
882 } else {
883 Q_EMIT q->fileCreationRejected(m_baseUrl);
884 }
885}
886
887void KNewFileMenuPrivate::slotActionTriggered(QAction *action)
888{
889 q->trigger(); // was for kdesktop's slotNewMenuActivated() in kde3 times. Can't hurt to keep it...
890
891 if (action == m_newDirAction) {
892 q->createDirectory();
893 return;
894 }
895 const int id = action->data().toInt();
896 Q_ASSERT(id > 0);
897
898 KNewFileMenuSingleton *s = kNewMenuGlobals();
899 const KNewFileMenuSingleton::Entry entry = s->templatesList->at(id - 1);
900
901 const bool createSymlink = entry.templatePath == QLatin1String("__CREATE_SYMLINK__");
902
903 m_copyData = KNewFileMenuCopyData();
904
905 if (createSymlink) {
906 m_copyData.m_isSymlink = true;
907 executeSymLink(entry);
908 } else if (KDesktopFile::isDesktopFile(entry.templatePath)) {
909 KDesktopFile df(entry.templatePath);
910 if (df.readType() == QLatin1String("Link")) {
911 executeUrlDesktopFile(entry);
912 } else { // any other desktop file (Device, App, etc.)
913 executeOtherDesktopFile(entry);
914 }
915 } else {
916 executeRealFileOrDir(entry);
917 }
918}
919
920void KNewFileMenuPrivate::slotCreateDirectory()
921{
922 // Automatically trim trailing spaces since they're pretty much always
923 // unintentional and can cause issues on Windows in shared environments
924 while (m_text.endsWith(QLatin1Char(' '))) {
925 m_text.chop(1);
926 }
927
928 QUrl url;
929 QUrl baseUrl = m_popupFiles.first();
930
931 QString name = expandTilde(m_text);
932
933 if (!name.isEmpty()) {
934 if (Utils::isAbsoluteLocalPath(name)) {
935 url = QUrl::fromLocalFile(name);
936 } else {
937 url = baseUrl;
938 url.setPath(Utils::concatPaths(url.path(), name));
939 }
940 }
941
942 KIO::Job *job;
943 if (name.contains(QLatin1Char('/'))) {
944 // If the name contains any slashes, use mkpath so that a/b/c works.
945 job = KIO::mkpath(url, baseUrl);
947 } else {
948 // If not, use mkdir so it will fail if the name of an existing folder was used
949 job = KIO::mkdir(url);
950 KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Mkdir, QList<QUrl>(), url, job);
951 }
952 job->setProperty("newDirectoryURL", url);
954 KJobWidgets::setWindow(job, m_parentWidget);
955
956 if (job) {
957 // We want the error handling to be done by slotResult so that subclasses can reimplement it
960 }
961 slotAbortDialog();
962}
963
964static QStringList getInstalledTemplates()
965{
968 static bool templateWarningShown = false;
969 // Some distros set TemplatesLocation to home dir, which means it hasn't been set up and should be ignored
970 // Otherwise everything in the home folder will be used as a template
971 if (templateFolder != QDir::homePath()) {
972 list << templateFolder;
973 } else if (!templateWarningShown) {
974 qCWarning(KFILEWIDGETS_LOG) << "Your 'templates' folder is set to your home folder. "
975 "This is probably an error in your settings. Ignoring it. "
976 "You can change the setting by running `systemsettings kcm_desktoppaths`. ";
977 templateWarningShown = true;
978 }
979 return list;
980}
981
982static QStringList getTemplateFilePaths(const QStringList &templates)
983{
984 QDir dir;
985 QStringList files;
986 for (const QString &path : templates) {
987 dir.setPath(path);
988 const QStringList entryList = dir.entryList(QDir::NoDotAndDotDot | QDir::AllEntries);
989 files.reserve(files.size() + entryList.size());
990 for (const QString &entry : entryList) {
991 const QString file = Utils::concatPaths(dir.path(), entry);
992 files.append(file);
993 }
994 }
995 return files;
996}
997
998void KNewFileMenuPrivate::slotFillTemplates()
999{
1000 KNewFileMenuSingleton *instance = kNewMenuGlobals();
1001 // qDebug();
1002
1003 const QStringList installedTemplates = getInstalledTemplates();
1004 const QStringList qrcTemplates{QStringLiteral(":/kio5/newfile-templates")};
1005 const QStringList templates = qrcTemplates + installedTemplates;
1006
1007 // Ensure any changes in the templates dir will call this
1008 if (!instance->dirWatch) {
1009 instance->dirWatch = std::make_unique<KDirWatch>();
1010 for (const QString &dir : installedTemplates) {
1011 instance->dirWatch->addDir(dir);
1012 }
1013
1014 auto slotFunc = [this]() {
1015 slotFillTemplates();
1016 };
1017 QObject::connect(instance->dirWatch.get(), &KDirWatch::dirty, q, slotFunc);
1018 QObject::connect(instance->dirWatch.get(), &KDirWatch::created, q, slotFunc);
1019 QObject::connect(instance->dirWatch.get(), &KDirWatch::deleted, q, slotFunc);
1020 // Ok, this doesn't cope with new dirs in XDG_DATA_DIRS, but that's another story
1021 }
1022
1023 // Look into "templates" dirs.
1024 QStringList files = getTemplateFilePaths(templates);
1025
1026 // Remove files that begin with a dot.
1027 // dir.entryList(QDir::NoDotAndDotDot | QDir::AllEntries) does not disregard internal files that
1028 // start with a dot like :/kio5/newfile-templates/.source
1029 auto removeFunc = [](const QString &path) {
1030 QFileInfo fileinfo(path);
1031 return fileinfo.fileName().startsWith(QLatin1Char('.'));
1032 };
1033 files.erase(std::remove_if(files.begin(), files.end(), removeFunc), files.end());
1034
1035 // Ensure desktop files are always before template files
1036 // This ensures consistent behavior
1037 std::partition(files.begin(), files.end(), [](const QString &a) {
1038 return a.endsWith(QStringLiteral(".desktop"));
1039 });
1040
1041 std::vector<EntryInfo> uniqueEntries;
1042 QMimeDatabase db;
1043 for (const QString &file : files) {
1044 // qDebug() << file;
1045 KNewFileMenuSingleton::Entry entry;
1046 entry.entryType = KNewFileMenuSingleton::Unknown; // not parsed yet
1047 QString url;
1048 QString key;
1049
1050 if (file.endsWith(QLatin1String(".desktop"))) {
1051 entry.filePath = file;
1052 const KDesktopFile config(file);
1053 url = config.desktopGroup().readEntry("URL");
1054 key = config.desktopGroup().readEntry("Name");
1055 }
1056 // Preparse non-.desktop files
1057 else {
1058 QFileInfo fileinfo(file);
1059 url = file;
1060 key = fileinfo.fileName();
1061 entry.entryType = KNewFileMenuSingleton::Template;
1062 entry.text = fileinfo.baseName();
1063 entry.filePath = fileinfo.completeBaseName();
1064 entry.templatePath = file;
1065 QMimeType mime = db.mimeTypeForFile(file);
1066 entry.mimeType = mime.name();
1067 entry.icon = mime.iconName();
1068 entry.comment = i18nc("@label:textbox Prompt for new file of type", "Enter %1 filename:", mime.comment());
1069 }
1070 // Put Directory first in the list (a bit hacky),
1071 // and TextFile before others because it's the most used one.
1072 // This also sorts by user-visible name.
1073 // The rest of the re-ordering is done in fillMenu.
1074 if (file.endsWith(QLatin1String("Directory.desktop"))) {
1075 key.prepend(QLatin1Char('0'));
1076 } else if (file.startsWith(QDir::homePath())) {
1077 key.prepend(QLatin1Char('1'));
1078 } else if (file.endsWith(QLatin1String("TextFile.desktop"))) {
1079 key.prepend(QLatin1Char('2'));
1080 } else {
1081 key.prepend(QLatin1Char('3'));
1082 }
1083
1084 EntryInfo eInfo = {key, url, entry};
1085 auto it = std::find_if(uniqueEntries.begin(), uniqueEntries.end(), [&url](const EntryInfo &info) {
1086 return url == info.url;
1087 });
1088
1089 if (it != uniqueEntries.cend()) {
1090 *it = eInfo;
1091 } else {
1092 uniqueEntries.push_back(eInfo);
1093 }
1094 }
1095
1096 std::sort(uniqueEntries.begin(), uniqueEntries.end(), [](const EntryInfo &a, const EntryInfo &b) {
1097 return a.key < b.key;
1098 });
1099
1100 ++instance->templatesVersion;
1101 instance->filesParsed = false;
1102
1103 instance->templatesList->clear();
1104
1105 instance->templatesList->reserve(uniqueEntries.size());
1106 for (const auto &info : uniqueEntries) {
1107 instance->templatesList->append(info.entry);
1108 };
1109}
1110
1111void KNewFileMenuPrivate::_k_slotOtherDesktopFile(KPropertiesDialog *sender)
1112{
1113 // The properties dialog took care of the copying, so we're done
1114 Q_EMIT q->fileCreated(sender->url());
1115}
1116
1117void KNewFileMenuPrivate::slotOtherDesktopFileClosed()
1118{
1119 QFile::remove(m_tempFileToDelete);
1120}
1121
1122void KNewFileMenuPrivate::slotRealFileOrDir()
1123{
1124 // Automatically trim trailing spaces since they're pretty much always
1125 // unintentional and can cause issues on Windows in shared environments
1126 while (m_text.endsWith(QLatin1Char(' '))) {
1127 m_text.chop(1);
1128 }
1129 m_copyData.m_chosenFileName = m_text;
1130 slotAbortDialog();
1131 executeStrategy();
1132}
1133
1134void KNewFileMenuPrivate::slotSymLink()
1135{
1136 KNameAndUrlInputDialog *dlg = static_cast<KNameAndUrlInputDialog *>(m_fileDialog);
1137
1138 m_copyData.m_chosenFileName = dlg->name(); // no path
1139 const QString linkTarget = dlg->urlText();
1140
1141 if (m_copyData.m_chosenFileName.isEmpty() || linkTarget.isEmpty()) {
1142 return;
1143 }
1144
1145 m_copyData.m_src = linkTarget;
1146 executeStrategy();
1147}
1148
1149void KNewFileMenuPrivate::_k_delayedSlotTextChanged()
1150{
1151 m_delayedSlotTextChangedTimer->start();
1152 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!m_lineEdit->text().isEmpty());
1153}
1154
1155void KNewFileMenuPrivate::_k_slotTextChanged(const QString &text)
1156{
1157 // Validate input, displaying a KMessageWidget for questionable names
1158
1159 if (text.isEmpty()) {
1160 m_messageWidget->hide();
1161 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1162 }
1163
1164 // Don't allow creating folders that would mask . or ..
1165 else if (text == QLatin1Char('.') || text == QLatin1String("..")) {
1166 m_messageWidget->setText(
1167 xi18nc("@info", "The name <filename>%1</filename> cannot be used because it is reserved for use by the operating system.", text));
1168 m_messageWidget->setMessageType(KMessageWidget::Error);
1169 m_messageWidget->animatedShow();
1170 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1171 }
1172
1173 // File or folder would be hidden; show warning
1174 else if (text.startsWith(QLatin1Char('.'))) {
1175 m_messageWidget->setText(xi18nc("@info", "The name <filename>%1</filename> starts with a dot, so it will be hidden by default.", text));
1176 m_messageWidget->setMessageType(KMessageWidget::Warning);
1177 m_messageWidget->animatedShow();
1178 }
1179
1180 // File or folder begins with a space; show warning
1181 else if (text.startsWith(QLatin1Char(' '))) {
1182 m_messageWidget->setText(xi18nc("@info",
1183 "The name <filename>%1</filename> starts with a space, which will result in it being shown before other items when "
1184 "sorting alphabetically, among other potential oddities.",
1185 text));
1186 m_messageWidget->setMessageType(KMessageWidget::Warning);
1187 m_messageWidget->animatedShow();
1188 }
1189#ifndef Q_OS_WIN
1190 // Inform the user that slashes in folder names create a directory tree
1191 else if (text.contains(QLatin1Char('/'))) {
1192 if (m_creatingDirectory) {
1193 QStringList folders = text.split(QLatin1Char('/'));
1194 if (!folders.isEmpty()) {
1195 if (folders.first().isEmpty()) {
1196 folders.removeFirst();
1197 }
1198 }
1199 QString label;
1200 if (folders.count() > 1) {
1201 label = i18n("Using slashes in folder names will create sub-folders, like so:");
1202 QString indentation = QString();
1203 for (const QString &folder : std::as_const(folders)) {
1204 label.append(QLatin1Char('\n'));
1205 label.append(indentation);
1206 label.append(folder);
1207 label.append(QStringLiteral("/"));
1208 indentation.append(QStringLiteral(" "));
1209 }
1210 } else {
1211 label = i18n("Using slashes in folder names will create sub-folders.");
1212 }
1213 m_messageWidget->setText(label);
1214 m_messageWidget->setMessageType(KMessageWidget::Information);
1215 m_messageWidget->animatedShow();
1216 }
1217 }
1218#endif
1219
1220#ifdef Q_OS_WIN
1221 // Slashes and backslashes are not allowed in Windows filenames; show error
1222 else if (text.contains(QLatin1Char('/'))) {
1223 m_messageWidget->setText(i18n("Slashes cannot be used in file and folder names."));
1224 m_messageWidget->setMessageType(KMessageWidget::Error);
1225 m_messageWidget->animatedShow();
1226 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1227 } else if (text.contains(QLatin1Char('\\'))) {
1228 m_messageWidget->setText(i18n("Backslashes cannot be used in file and folder names."));
1229 m_messageWidget->setMessageType(KMessageWidget::Error);
1230 m_messageWidget->animatedShow();
1231 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1232 }
1233#endif
1234
1235 // Using a tilde to begin a file or folder name is not recommended
1236 else if (text.startsWith(QLatin1Char('~'))) {
1237 m_messageWidget->setText(
1238 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 "
1239 "things."));
1240 m_messageWidget->setMessageType(KMessageWidget::Warning);
1241 m_messageWidget->animatedShow();
1242 } else {
1243 m_messageWidget->hide();
1244 }
1245
1246 if (!text.isEmpty()) {
1247 // Check file does not already exists
1248 m_statRunning = true;
1249 QUrl url;
1250 if (m_creatingDirectory && text.at(0) == QLatin1Char('~')) {
1252 } else {
1253 url = QUrl(m_baseUrl.toString() + QLatin1Char('/') + text);
1254 }
1255 KIO::StatJob *job = KIO::stat(url, KIO::StatJob::StatSide::DestinationSide, KIO::StatDetail::StatBasic, KIO::HideProgressInfo);
1256 QObject::connect(job, &KJob::result, m_fileDialog, [this](KJob *job) {
1257 _k_slotStatResult(job);
1258 });
1259 job->start();
1260 }
1261
1262 m_text = text;
1263}
1264
1265void KNewFileMenu::setSelectDirWhenAlreadyExist(bool shouldSelectExistingDir)
1266{
1267 d->m_selectDirWhenAlreadyExists = shouldSelectExistingDir;
1268}
1269
1270void KNewFileMenuPrivate::_k_slotStatResult(KJob *job)
1271{
1272 m_statRunning = false;
1273 KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
1274 // ignore stat Result when the lineEdit has changed
1275 const QUrl url = statJob->url().adjusted(QUrl::StripTrailingSlash);
1276 if (m_creatingDirectory && m_lineEdit->text().startsWith(QLatin1Char('~'))) {
1277 if (url.path() != KShell::tildeExpand(m_lineEdit->text())) {
1278 return;
1279 }
1280 } else if (url.fileName() != m_lineEdit->text()) {
1281 return;
1282 }
1283 bool accepted = m_acceptedPressed;
1284 m_acceptedPressed = false;
1285 auto error = job->error();
1286 if (error) {
1287 if (error == KIO::ERR_DOES_NOT_EXIST) {
1288 // fine for file creation
1289 if (accepted) {
1290 m_fileDialog->accept();
1291 }
1292 } else {
1293 qWarning() << error << job->errorString();
1294 }
1295 } else {
1296 bool shouldEnable = false;
1298
1299 const KIO::UDSEntry &entry = statJob->statResult();
1300 if (entry.isDir()) {
1301 if (m_selectDirWhenAlreadyExists && m_creatingDirectory) {
1302 // allow "overwrite" of dir
1303 messageType = KMessageWidget::Information;
1304 shouldEnable = true;
1305 }
1306 m_messageWidget->setText(xi18nc("@info", "A directory with name <filename>%1</filename> already exists.", m_text));
1307 } else {
1308 m_messageWidget->setText(xi18nc("@info", "A file with name <filename>%1</filename> already exists.", m_text));
1309 }
1310 m_messageWidget->setMessageType(messageType);
1311 m_messageWidget->animatedShow();
1312 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(shouldEnable);
1313
1314 if (accepted && shouldEnable) {
1315 m_fileDialog->accept();
1316 }
1317 }
1318}
1319
1320void KNewFileMenuPrivate::slotUrlDesktopFile()
1321{
1322 KNameAndUrlInputDialog *dlg = static_cast<KNameAndUrlInputDialog *>(m_fileDialog);
1323 QString name = dlg->name();
1324 const QLatin1String ext(".desktop");
1325 if (!name.endsWith(ext)) {
1326 name += ext;
1327 }
1328 m_copyData.m_chosenFileName = name; // no path
1329 QUrl linkUrl = dlg->url();
1330
1331 // Filter user input so that short uri entries, e.g. www.kde.org, are
1332 // handled properly. This not only makes the icon detection below work
1333 // properly, but opening the URL link where the short uri will not be
1334 // sent to the application (opening such link Konqueror fails).
1335 KUriFilterData uriData;
1336 uriData.setData(linkUrl); // the url to put in the file
1337 uriData.setCheckForExecutables(false);
1338
1339 if (KUriFilter::self()->filterUri(uriData, QStringList{QStringLiteral("kshorturifilter")})) {
1340 linkUrl = uriData.uri();
1341 }
1342
1343 if (m_copyData.m_chosenFileName.isEmpty() || linkUrl.isEmpty()) {
1344 return;
1345 }
1346
1347 // It's a "URL" desktop file; we need to make a temp copy of it, to modify it
1348 // before copying it to the final destination [which could be a remote protocol]
1349 QTemporaryFile tmpFile;
1350 tmpFile.setAutoRemove(false); // done below
1351 if (!tmpFile.open()) {
1352 qCritical() << "Couldn't create temp file!";
1353 return;
1354 }
1355
1356 if (!checkSourceExists(m_copyData.m_templatePath)) {
1357 return;
1358 }
1359
1360 // First copy the template into the temp file
1361 QFile file(m_copyData.m_templatePath);
1362 if (!file.open(QIODevice::ReadOnly)) {
1363 qCritical() << "Couldn't open template" << m_copyData.m_templatePath;
1364 return;
1365 }
1366 const QByteArray data = file.readAll();
1367 tmpFile.write(data);
1368 const QString tempFileName = tmpFile.fileName();
1369 Q_ASSERT(!tempFileName.isEmpty());
1370 tmpFile.close();
1371 file.close();
1372
1373 KDesktopFile df(tempFileName);
1374 KConfigGroup group = df.desktopGroup();
1375
1376 if (linkUrl.isLocalFile()) {
1377 KFileItem fi(linkUrl);
1378 group.writeEntry("Icon", fi.iconName());
1379 } else {
1380 group.writeEntry("Icon", KProtocolInfo::icon(linkUrl.scheme()));
1381 }
1382
1383 group.writePathEntry("URL", linkUrl.toDisplayString());
1384 group.writeEntry("Name", dlg->name()); // Used as user-visible name by kio_desktop
1385 df.sync();
1386
1387 m_copyData.m_src = tempFileName;
1388 m_copyData.m_tempFileToDelete = tempFileName;
1389
1390 executeStrategy();
1391}
1392
1394 : KActionMenu(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Create New"), parent)
1395 , d(std::make_unique<KNewFileMenuPrivate>(this))
1396{
1397 // Don't fill the menu yet
1398 // We'll do that in checkUpToDate (should be connected to aboutToShow)
1399 d->m_newMenuGroup = new QActionGroup(this);
1400 connect(d->m_newMenuGroup, &QActionGroup::triggered, this, [this](QAction *action) {
1401 d->slotActionTriggered(action);
1402 });
1403
1404 // Connect directory creation signals
1405 connect(this, &KNewFileMenu::directoryCreationStarted, this, [this] {
1406 d->m_isCreateDirectoryRunning = true;
1407 });
1408 connect(this, &KNewFileMenu::directoryCreated, this, [this] {
1409 d->m_isCreateDirectoryRunning = false;
1410 });
1412 d->m_isCreateDirectoryRunning = false;
1413 });
1414
1415 // Connect file creation signals
1416 connect(this, &KNewFileMenu::fileCreationStarted, this, [this] {
1417 d->m_isCreateFileRunning = true;
1418 });
1419 connect(this, &KNewFileMenu::fileCreated, this, [this] {
1420 d->m_isCreateFileRunning = false;
1421 });
1422 connect(this, &KNewFileMenu::fileCreationRejected, this, [this] {
1423 d->m_isCreateFileRunning = false;
1424 });
1425
1426 d->m_parentWidget = qobject_cast<QWidget *>(parent);
1427 d->m_newDirAction = nullptr;
1428
1429 d->m_menuDev = new KActionMenu(QIcon::fromTheme(QStringLiteral("drive-removable-media")), i18n("Link to Device"), this);
1430}
1431
1432KNewFileMenu::~KNewFileMenu() = default;
1433
1435{
1436 KNewFileMenuSingleton *s = kNewMenuGlobals();
1437 // qDebug() << this << "m_menuItemsVersion=" << d->m_menuItemsVersion
1438 // << "s->templatesVersion=" << s->templatesVersion;
1439 if (d->m_menuItemsVersion < s->templatesVersion || s->templatesVersion == 0) {
1440 // qDebug() << "recreating actions";
1441 // We need to clean up the action collection
1442 // We look for our actions using the group
1443 qDeleteAll(d->m_newMenuGroup->actions());
1444
1445 if (!s->templatesList) { // No templates list up to now
1446 s->templatesList = new KNewFileMenuSingleton::EntryList;
1447 d->slotFillTemplates();
1448 s->parseFiles();
1449 }
1450
1451 // This might have been already done for other popupmenus,
1452 // that's the point in s->filesParsed.
1453 if (!s->filesParsed) {
1454 s->parseFiles();
1455 }
1456
1457 d->fillMenu();
1458
1459 d->m_menuItemsVersion = s->templatesVersion;
1460 }
1461}
1462
1464{
1465 if (d->m_popupFiles.isEmpty()) {
1466 return;
1467 }
1468
1469 d->m_baseUrl = d->m_popupFiles.first();
1470
1471 if (d->m_isCreateDirectoryRunning) {
1472 qCWarning(KFILEWIDGETS_LOG) << "Directory creation is already running for " << d->m_baseUrl;
1473 }
1474
1475 QString name = !d->m_text.isEmpty() ? d->m_text : i18nc("Default name for a new folder", "New Folder");
1476
1477 auto nameJob = new KIO::NameFinderJob(d->m_baseUrl, name, this);
1478 connect(nameJob, &KJob::result, this, [nameJob, name, this]() mutable {
1479 if (!nameJob->error()) {
1480 d->m_baseUrl = nameJob->baseUrl();
1481 name = nameJob->finalName();
1482 }
1483 d->showNewDirNameDlg(name);
1484 });
1485 nameJob->start();
1486 Q_EMIT directoryCreationStarted(d->m_baseUrl);
1487}
1488
1490{
1491 return d->m_isCreateDirectoryRunning;
1492}
1493
1494void KNewFileMenuPrivate::showNewDirNameDlg(const QString &name)
1495{
1496 initDialog();
1497
1498 m_fileDialog->setWindowTitle(m_windowTitle.isEmpty() ? i18nc("@title:window", "Create New Folder") : m_windowTitle);
1499
1500 m_label->setText(i18n("Create new folder in %1:", m_baseUrl.toDisplayString(QUrl::PreferLocalFile)));
1501
1502 m_lineEdit->setText(name);
1503
1504 m_creatingDirectory = true;
1505 _k_slotTextChanged(name); // have to save string in m_text in case user does not touch dialog
1506 QObject::connect(m_lineEdit, &QLineEdit::textChanged, q, [this]() {
1507 _k_delayedSlotTextChanged();
1508 });
1509 m_delayedSlotTextChangedTimer->callOnTimeout(m_lineEdit, [this]() {
1510 _k_slotTextChanged(m_lineEdit->text());
1511 });
1512
1513 QObject::connect(m_fileDialog, &QDialog::accepted, q, [this]() {
1514 slotCreateDirectory();
1515 });
1516 QObject::connect(m_fileDialog, &QDialog::rejected, q, [this]() {
1517 slotAbortDialog();
1518 });
1519
1520 m_fileDialog->show();
1521 m_lineEdit->selectAll();
1522 m_lineEdit->setFocus();
1523}
1524
1526{
1527 if (d->m_popupFiles.isEmpty()) {
1529 return;
1530 }
1531
1532 checkUpToDate();
1533 if (!d->m_firstFileEntry) {
1535 return;
1536 }
1537
1538 if (!d->m_isCreateFileRunning) {
1539 d->executeRealFileOrDir(*d->m_firstFileEntry);
1540 } else {
1541 qCWarning(KFILEWIDGETS_LOG) << "File creation is already running for " << d->m_firstFileEntry;
1542 }
1543}
1544
1546{
1547 return d->m_isCreateFileRunning;
1548}
1549
1551{
1552 return d->m_modal;
1553}
1554
1556{
1557 d->m_modal = modal;
1558}
1559
1561{
1562 d->m_parentWidget = parentWidget;
1563}
1564
1566{
1567 d->m_supportedMimeTypes = mime;
1568}
1569
1571{
1572 d->m_windowTitle = title;
1573}
1574
1576{
1577 if (job->error()) {
1578 if (job->error() == KIO::ERR_DIR_ALREADY_EXIST && d->m_selectDirWhenAlreadyExists) {
1579 auto *simpleJob = ::qobject_cast<KIO::SimpleJob *>(job);
1580 if (simpleJob) {
1581 const QUrl jobUrl = simpleJob->url();
1582 // Select the existing dir
1583 Q_EMIT selectExistingDir(jobUrl);
1584 }
1585 } else { // All other errors
1586 static_cast<KIO::Job *>(job)->uiDelegate()->showErrorMessage();
1587 }
1588 } else {
1589 // Was this a copy or a mkdir?
1590 if (job->property("newDirectoryURL").isValid()) {
1591 QUrl newDirectoryURL = job->property("newDirectoryURL").toUrl();
1592 Q_EMIT directoryCreated(newDirectoryURL);
1593 } else {
1595 if (copyJob) {
1596 const QUrl destUrl = copyJob->destUrl();
1597 const QUrl localUrl = d->mostLocalUrl(destUrl);
1598 if (localUrl.isLocalFile()) {
1599 // Normal (local) file. Need to "touch" it, kio_file copied the mtime.
1600 (void)::utime(QFile::encodeName(localUrl.toLocalFile()).constData(), nullptr);
1601 }
1602 Q_EMIT fileCreated(destUrl);
1603 } else if (KIO::SimpleJob *simpleJob = ::qobject_cast<KIO::SimpleJob *>(job)) {
1604 // Called in the storedPut() case
1605#ifdef WITH_QTDBUS
1606 org::kde::KDirNotify::emitFilesAdded(simpleJob->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
1607#endif
1608 Q_EMIT fileCreated(simpleJob->url());
1609 }
1610 }
1611 }
1612 if (!d->m_tempFileToDelete.isEmpty()) {
1613 QFile::remove(d->m_tempFileToDelete);
1614 }
1615}
1616
1618{
1619 return d->m_supportedMimeTypes;
1620}
1621
1623{
1624 d->m_popupFiles = {directory};
1625
1626 if (directory.isEmpty()) {
1627 d->m_newMenuGroup->setEnabled(false);
1628 } else {
1629 if (KProtocolManager::supportsWriting(directory)) {
1630 d->m_newMenuGroup->setEnabled(true);
1631 if (d->m_newDirAction) {
1632 d->m_newDirAction->setEnabled(KProtocolManager::supportsMakeDir(directory)); // e.g. trash:/
1633 }
1634 } else {
1635 d->m_newMenuGroup->setEnabled(true);
1636 }
1637 }
1638}
1639
1641{
1642 return d->m_popupFiles.isEmpty() ? QUrl() : d->m_popupFiles.first();
1643}
1644
1646{
1647 d->m_newFolderShortcutAction = action;
1648}
1649
1651{
1652 d->m_newFileShortcutAction = action;
1653}
1654
1655#include "moc_knewfilemenu.cpp"
void writeEntry(const char *key, const char *value, WriteConfigFlags pFlags=Normal)
void writePathEntry(const char *Key, const QString &path, WriteConfigFlags pFlags=Normal)
static bool isDesktopFile(const QString &path)
void deleted(const QString &path)
void dirty(const QString &path)
void created(const QString &path)
CopyJob is used to move, copy or symlink files and directories.
Definition copyjob.h:41
void setDefaultPermissions(bool b)
By default the permissions of the copied files will be those of the source files.
Definition copyjob.cpp:2608
QUrl destUrl() const
Returns the destination URL.
Definition copyjob.cpp:457
void recordCopyJob(KIO::CopyJob *copyJob)
Record this CopyJob while it's happening and add a command for it so that the user can undo it.
static FileUndoManager * self()
void recordJob(CommandType op, const QList< QUrl > &src, const QUrl &dst, KIO::Job *job)
Record this job while it's happening and add a command for it so that the user can undo it.
@ Put
Represents the creation of a file from data in memory. Used when pasting data from clipboard or drag-...
@ Mkpath
Represents a KIO::mkpath() job.
The base class for all jobs.
Definition job_base.h:45
NameFinderJob finds a valid "New Folder" name.
A simple job (one url and one command).
Definition simplejob.h:27
const QUrl & url() const
Returns the SimpleJob's URL.
Definition simplejob.cpp:70
A KIO job that retrieves information about a file or directory.
Definition statjob.h:26
const UDSEntry & statResult() const
Result of the stat operation.
Definition statjob.cpp:80
QUrl mostLocalUrl() const
most local URL
Definition statjob.cpp:85
bool isDir() const
Definition udsentry.cpp:375
void setAutoErrorHandlingEnabled(bool enable)
bool exec()
virtual QString errorString() const
int error() const
void result(KJob *job)
KJobUiDelegate * uiDelegate() const
virtual Q_SCRIPTABLE void start()=0
void setModal(bool modality)
Sets the modality of dialogs created by KNewFile.
~KNewFileMenu() override
Destructor.
void createFile()
Call this to create a new file as if the user had done it using a popupmenu.
QUrl workingDirectory() const
Returns the working directory.
KNewFileMenu(QObject *parent)
Constructor.
void directoryCreationStarted(const QUrl &url)
Emitted once the creation job for directory url has been started.
bool isCreateDirectoryRunning()
Use this to check if namejob for new directory creation still running.
void setNewFileShortcutAction(QAction *action)
Use this to set a shortcut for the new file action.
void setSelectDirWhenAlreadyExist(bool b)
Whether on not the dialog should emit selectExistingDir when trying to create an exist directory.
void setWorkingDirectory(const QUrl &directory)
Set the working directory.
void checkUpToDate()
Checks if updating the list is necessary IMPORTANT : Call this in the slot for aboutToShow.
void directoryCreated(const QUrl &url)
Emitted once the directory url has been successfully created.
QStringList supportedMimeTypes() const
Returns the MIME types set in supportedMimeTypes()
void directoryCreationRejected(const QUrl &url)
Emitted once the creation for directory url has been rejected.
void setParentWidget(QWidget *parentWidget)
Sets a parent widget for the dialogs shown by KNewFileMenu.
void fileCreationRejected(const QUrl &url)
Emitted once the creation for file url has been rejected.
bool isCreateFileRunning()
Use this to check if the file creation process is still running.
void setWindowTitle(const QString &title)
Allow to change the popup.
virtual void slotResult(KJob *job)
Called when the job that copied the template has finished.
void setNewFolderShortcutAction(QAction *action)
Use this to set a shortcut for the "New Folder" action.
void selectExistingDir(const QUrl &url)
Emitted when trying to create a new directory that has the same name as an existing one,...
void setSupportedMimeTypes(const QStringList &mime)
Only show the files in a given set of MIME types.
void fileCreated(const QUrl &url)
Emitted once the file (or symlink) url has been successfully created.
bool isModal() const
Returns the modality of dialogs.
void createDirectory()
Call this to create a new directory as if the user had done it using a popupmenu.
void fileCreationStarted(const QUrl &url)
Emitted once the creation job for file url has been started.
The main properties dialog class.
void applied()
This signal is emitted when the properties changes are applied (for example, with the OK button)
void propertiesClosed()
This signal is emitted when the Properties Dialog is closed (for example, with OK or Cancel buttons)
QUrl url() const
The URL of the file that has its properties being displayed.
static QString protocolClass(const QString &protocol)
Returns the protocol class for the specified protocol.
static QString icon(const QString &protocol)
Returns the name of the icon, associated with the specified protocol.
static bool supportsMakeDir(const QUrl &url)
Returns whether the protocol can create directories/folders.
static bool supportsWriting(const QUrl &url)
Returns whether the protocol can store data to URLs.
QUrl uri() const
Returns the filtered or the original URL.
void setCheckForExecutables(bool check)
Check whether the provided uri is executable or not.
void setData(const QUrl &url)
Same as above except the argument is a URL.
static KUriFilter * self()
Returns an instance of KUriFilter.
QString xi18nc(const char *context, const char *text, const TYPE &arg...)
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
KCOREADDONS_EXPORT QString suggestName(const QUrl &baseURL, const QString &oldName)
KIOCORE_EXPORT StoredTransferJob * storedPut(QIODevice *input, const QUrl &url, int permissions, JobFlags flags=DefaultFlags)
Put (means: write) data from a QIODevice.
KIOCORE_EXPORT MkdirJob * mkdir(const QUrl &url, int permissions=-1)
Creates a single directory.
Definition mkdirjob.cpp:110
KIOCORE_EXPORT StatJob * stat(const QUrl &url, JobFlags flags=DefaultFlags)
Find all details for one file or directory.
Definition statjob.cpp:203
KIOCORE_EXPORT StatJob * mostLocalUrl(const QUrl &url, JobFlags flags=DefaultFlags)
Tries to map a local URL for the given URL, using a KIO job.
Definition statjob.cpp:193
KIOCORE_EXPORT CopyJob * linkAs(const QUrl &src, const QUrl &dest, JobFlags flags=DefaultFlags)
Create a link.
Definition copyjob.cpp:2703
@ HideProgressInfo
Hide progress information dialog, i.e. don't show a GUI.
Definition job_base.h:251
@ StatBasic
Filename, access, type, size, linkdest.
Definition global.h:255
KIOCORE_EXPORT MkpathJob * mkpath(const QUrl &url, const QUrl &baseUrl=QUrl(), JobFlags flags=DefaultFlags)
Creates a directory, creating parent directories as needed.
KIOCORE_EXPORT CopyJob * copyAs(const QUrl &src, const QUrl &dest, JobFlags flags=DefaultFlags)
Copy a file or directory src into the destination dest, which is the destination name in any case,...
Definition copyjob.cpp:2643
KIOCORE_EXPORT QString encodeFileName(const QString &str)
Encodes (from the text displayed to the real filename) This translates '/' into a "unicode fraction s...
Definition global.cpp:111
QString name(const QVariant &location)
void setWindow(QObject *job, QWidget *widget)
QString path(const QString &relativePath)
QDialogButtonBox::StandardButton createKMessageBox(QDialog *dialog, QDialogButtonBox *buttons, const QIcon &icon, const QString &text, const QStringList &strlist, const QString &ask, bool *checkboxReturn, Options options, const QString &details=QString(), QMessageBox::Icon notifyType=QMessageBox::Information)
void error(QWidget *parent, const QString &text, const QString &title, const KGuiItem &buttonOk, Options options=Notify)
KIOCORE_EXPORT QString dir(const QString &fileClass)
Returns the most recently used directory associated with this file-class.
KIOCORE_EXPORT QStringList list(const QString &fileClass)
Returns a list of directories associated with this file-class.
KCOREADDONS_EXPORT QString tildeExpand(const QString &path)
QString label(StandardShortcut id)
QWidget * parentWidget() const const
QAction(QObject *parent)
void changed()
QVariant data() const const
void setIcon(const QIcon &icon)
void setActionGroup(QActionGroup *group)
void setData(const QVariant &data)
void setShortcuts(QKeySequence::StandardKey key)
void setShortcutContext(Qt::ShortcutContext context)
void setText(const QString &text)
void accepted()
void finished(int result)
void setModal(bool modal)
virtual void reject()
void rejected()
void setStandardButtons(StandardButtons buttons)
NoDotAndDotDot
QString homePath()
QByteArray encodeName(const QString &fileName)
bool exists() const const
bool remove()
virtual void close() override
QIcon fromTheme(const QString &name)
qint64 write(const QByteArray &data)
void setText(const QString &)
void textChanged(const QString &text)
void append(QList< T > &&value)
const_reference at(qsizetype i) const const
iterator begin()
void clear()
qsizetype count() const const
iterator end()
iterator erase(const_iterator begin, const_iterator end)
T & first()
bool isEmpty() const const
void removeFirst()
void reserve(qsizetype size)
qsizetype size() const const
T value(qsizetype i) const const
QAction * addAction(const QIcon &icon, const QString &text, Functor functor, const QKeySequence &shortcut)
QAction * addSeparator()
void clear()
QMimeType mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const const
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
QString suffixForFileName(const QString &fileName) const const
bool inherits(const QString &mimeTypeName) const const
Q_EMITQ_EMIT
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
QObject * parent() const const
QVariant property(const char *name) const const
T qobject_cast(QObject *object)
void setObjectName(QAnyStringView name)
bool setProperty(const char *name, QVariant &&value)
QStringList locateAll(StandardLocation type, const QString &fileName, LocateOptions options)
QStringList standardLocations(StandardLocation type)
QString & append(QChar ch)
const QChar at(qsizetype position) const const
void chop(qsizetype n)
bool contains(QChar ch, Qt::CaseSensitivity cs) const const
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString first(qsizetype n) const const
bool isEmpty() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QString & prepend(QChar ch)
QString & remove(QChar ch, Qt::CaseSensitivity cs)
qsizetype size() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QString trimmed() const const
WidgetShortcut
WA_DeleteOnClose
QTemporaryFile * createNativeFile(QFile &file)
virtual QString fileName() const const override
void setAutoRemove(bool b)
StripTrailingSlash
QUrl adjusted(FormattingOptions options) const const
QString fileName(ComponentFormattingOptions options) const const
QUrl fromLocalFile(const QString &localFile)
QUrl fromUserInput(const QString &userInput, const QString &workingDirectory, UserInputResolutionOptions options)
bool isEmpty() const const
bool isLocalFile() const const
QString path(ComponentFormattingOptions options) const const
QString scheme() const const
void setPath(const QString &path, ParsingMode mode)
QString toDisplayString(FormattingOptions options) const const
QString toLocalFile() const const
QString url(FormattingOptions options) const const
bool isValid() const const
int toInt(bool *ok) const const
QUrl toUrl() const const
void setAttribute(Qt::WidgetAttribute attribute, bool on)
void show()
void setWindowTitle(const QString &)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 21 2025 11:50:10 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.