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

source code of kio/src/filewidgets/knewfilemenu.cpp