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
398 KNewFileMenuSingleton::Entry *m_firstFileEntry = nullptr;
399
400 KNewFileMenu *const q;
401
402 KNewFileMenuCopyData m_copyData;
403
404 /**
405 * Use to delay a bit feedback to user
406 */
407 QTimer *m_delayedSlotTextChangedTimer;
408
409 QUrl m_baseUrl;
410
411 bool m_selectDirWhenAlreadyExists = false;
412 bool m_acceptedPressed = false;
413 bool m_statRunning = false;
414 bool m_isCreateDirectoryRunning = false;
415 bool m_isCreateFileRunning = false;
416};
417
418void KNewFileMenuPrivate::_k_slotAccepted()
419{
420 if (m_statRunning || m_delayedSlotTextChangedTimer->isActive()) {
421 // stat is running or _k_slotTextChanged has not been called already
422 // delay accept until stat has been run
423 m_acceptedPressed = true;
424
425 if (m_delayedSlotTextChangedTimer->isActive()) {
426 m_delayedSlotTextChangedTimer->stop();
427 _k_slotTextChanged(m_lineEdit->text());
428 }
429 } else {
430 m_fileDialog->accept();
431 }
432}
433
434void KNewFileMenuPrivate::initDialog()
435{
436 m_fileDialog = new QDialog(m_parentWidget);
437 m_fileDialog->setAttribute(Qt::WA_DeleteOnClose);
438 m_fileDialog->setModal(m_modal);
440 m_fileDialog->setWindowTitle(i18nc("@title:window", "Create New File"));
441
442 m_messageWidget = new KMessageWidget(m_fileDialog);
443 m_messageWidget->setCloseButtonVisible(false);
444 m_messageWidget->setWordWrap(true);
445 m_messageWidget->hide();
446
447 m_label = new QLabel(m_fileDialog);
448
449 m_lineEdit = new QLineEdit(m_fileDialog);
450 m_lineEdit->setClearButtonEnabled(true);
451 m_lineEdit->setMinimumWidth(400);
452
453 m_buttonBox = new QDialogButtonBox(m_fileDialog);
455 QObject::connect(m_buttonBox, &QDialogButtonBox::accepted, [this]() {
456 _k_slotAccepted();
457 });
458 QObject::connect(m_buttonBox, &QDialogButtonBox::rejected, m_fileDialog, &QDialog::reject);
459
460 QObject::connect(m_fileDialog, &QDialog::finished, m_fileDialog, [this] {
461 m_statRunning = false;
462 });
463
464 QVBoxLayout *layout = new QVBoxLayout(m_fileDialog);
465 layout->setSizeConstraint(QLayout::SetFixedSize);
466
467 layout->addWidget(m_label);
468 layout->addWidget(m_lineEdit);
469 layout->addWidget(m_buttonBox);
470 layout->addWidget(m_messageWidget);
471 layout->addStretch();
472}
473
474bool KNewFileMenuPrivate::checkSourceExists(const QString &src)
475{
476 if (!QFile::exists(src)) {
477 qWarning() << src << "doesn't exist";
478
479 QDialog *dialog = new QDialog(m_parentWidget);
480 dialog->setWindowTitle(i18n("Sorry"));
481 dialog->setObjectName(QStringLiteral("sorry"));
482 dialog->setModal(q->isModal());
484
485 QDialogButtonBox *box = new QDialogButtonBox(dialog);
487
489 box,
491 i18n("<qt>The template file <b>%1</b> does not exist.</qt>", src),
492 QStringList(),
493 QString(),
494 nullptr,
496
497 dialog->show();
498
499 return false;
500 }
501 return true;
502}
503
504void KNewFileMenuPrivate::executeOtherDesktopFile(const KNewFileMenuSingleton::Entry &entry)
505{
506 if (!checkSourceExists(entry.templatePath)) {
507 return;
508 }
509
510 for (const auto &url : std::as_const(m_popupFiles)) {
511 QString text = entry.text;
512 text.remove(QStringLiteral("...")); // the ... is fine for the menu item but not for the default filename
513 text = text.trimmed(); // In some languages, there is a space in front of "...", see bug 268895
514 // KDE5 TODO: remove the "..." from link*.desktop files and use i18n("%1...") when making
515 // the action.
516 QString name = text;
517 text.append(QStringLiteral(".desktop"));
518
519 const QUrl directory = mostLocalUrl(url);
520 const QUrl defaultFile = QUrl::fromLocalFile(directory.toLocalFile() + QLatin1Char('/') + KIO::encodeFileName(text));
521 if (defaultFile.isLocalFile() && QFile::exists(defaultFile.toLocalFile())) {
522 text = KFileUtils::suggestName(directory, text);
523 }
524
525 QUrl templateUrl;
526 bool usingTemplate = false;
527 if (entry.templatePath.startsWith(QLatin1String(":/"))) {
528 QTemporaryFile *tmpFile = QTemporaryFile::createNativeFile(entry.templatePath);
529 tmpFile->setAutoRemove(false);
530 QString tempFileName = tmpFile->fileName();
531 tmpFile->close();
532
533 KDesktopFile df(tempFileName);
534 KConfigGroup group = df.desktopGroup();
535 group.writeEntry("Name", name);
536 templateUrl = QUrl::fromLocalFile(tempFileName);
537 m_tempFileToDelete = tempFileName;
538 usingTemplate = true;
539 } else {
540 templateUrl = QUrl::fromLocalFile(entry.templatePath);
541 }
542 KPropertiesDialog *dlg = new KPropertiesDialog(templateUrl, directory, text, m_parentWidget);
543 dlg->setModal(q->isModal());
545 QObject::connect(dlg, &KPropertiesDialog::applied, q, [this, dlg]() {
546 _k_slotOtherDesktopFile(dlg);
547 });
548 if (usingTemplate) {
550 slotOtherDesktopFileClosed();
551 });
552 }
553 dlg->show();
554 }
555 // We don't set m_src here -> there will be no copy, we are done.
556}
557
558void KNewFileMenuPrivate::executeRealFileOrDir(const KNewFileMenuSingleton::Entry &entry)
559{
560 Q_EMIT q->fileCreationStarted(QUrl(entry.filePath));
561
562 initDialog();
563
564 const auto getSelectionLength = [](const QString &text) {
565 // Select the text without MIME-type extension
566 int selectionLength = text.length();
567
568 QMimeDatabase db;
569 const QString extension = db.suffixForFileName(text);
570 if (extension.isEmpty()) {
571 // For an unknown extension just exclude the extension after
572 // the last point. This does not work for multiple extensions like
573 // *.tar.gz but usually this is anyhow a known extension.
574 selectionLength = text.lastIndexOf(QLatin1Char('.'));
575
576 // If no point could be found, use whole text length for selection.
577 if (selectionLength < 1) {
578 selectionLength = text.length();
579 }
580
581 } else {
582 selectionLength -= extension.length() + 1;
583 }
584
585 return selectionLength;
586 };
587
588 // The template is not a desktop file
589 // Prompt the user to set the destination filename
590 QString text = entry.text;
591 text.remove(QStringLiteral("...")); // the ... is fine for the menu item but not for the default filename
592 text = text.trimmed(); // In some languages, there is a space in front of "...", see bug 268895
593 // add the extension (from the templatePath), should work with .txt, .html and with ".tar.gz"... etc
594 const QString fileName = entry.templatePath.mid(entry.templatePath.lastIndexOf(QLatin1Char('/')));
595 const int dotIndex = getSelectionLength(fileName);
596 text += dotIndex > 0 ? fileName.mid(dotIndex) : QString();
597
598 m_copyData.m_src = entry.templatePath;
599
600 const QUrl directory = mostLocalUrl(m_popupFiles.first());
601 m_baseUrl = directory;
602 const QUrl defaultFile = QUrl::fromLocalFile(directory.toLocalFile() + QLatin1Char('/') + KIO::encodeFileName(text));
603 if (defaultFile.isLocalFile() && QFile::exists(defaultFile.toLocalFile())) {
604 text = KFileUtils::suggestName(directory, text);
605 }
606
607 m_label->setText(entry.comment);
608
609 m_lineEdit->setText(text);
610
611 m_creatingDirectory = false;
612 _k_slotTextChanged(text);
613 QObject::connect(m_lineEdit, &QLineEdit::textChanged, q, [this]() {
614 _k_delayedSlotTextChanged();
615 });
616 m_delayedSlotTextChangedTimer->callOnTimeout(m_lineEdit, [this]() {
617 _k_slotTextChanged(m_lineEdit->text());
618 });
619
620 QObject::connect(m_fileDialog, &QDialog::accepted, q, [this]() {
621 slotRealFileOrDir();
622 });
623 QObject::connect(m_fileDialog, &QDialog::rejected, q, [this]() {
624 slotAbortDialog();
625 });
626
627 m_fileDialog->show();
628
629 const int firstDotInBaseName = getSelectionLength(text);
630 m_lineEdit->setSelection(0, firstDotInBaseName > 0 ? firstDotInBaseName : text.size());
631
632 m_lineEdit->setFocus();
633}
634
635void KNewFileMenuPrivate::executeSymLink(const KNewFileMenuSingleton::Entry &entry)
636{
637 KNameAndUrlInputDialog *dlg = new KNameAndUrlInputDialog(i18n("Name for new link:"), entry.comment, m_popupFiles.first(), m_parentWidget);
638 dlg->setModal(q->isModal());
640 dlg->setWindowTitle(i18n("Create Symlink"));
641 m_fileDialog = dlg;
642 QObject::connect(dlg, &QDialog::accepted, q, [this]() {
643 slotSymLink();
644 });
645 dlg->show();
646}
647
648void KNewFileMenuPrivate::executeStrategy()
649{
650 m_tempFileToDelete = m_copyData.tempFileToDelete();
651 const QString src = m_copyData.sourceFileToCopy();
652 QString chosenFileName = expandTilde(m_copyData.chosenFileName(), true);
653
654 if (src.isEmpty()) {
655 return;
656 }
657 QUrl uSrc(QUrl::fromLocalFile(src));
658
659 // In case the templates/.source directory contains symlinks, resolve
660 // them to the target files. Fixes bug #149628.
661 KFileItem item(uSrc, QString(), KFileItem::Unknown);
662 if (item.isLink()) {
663 uSrc.setPath(item.linkDest());
664 }
665
666 // The template is not a desktop file [or it's a URL one] >>> Copy it
667 for (const auto &u : std::as_const(m_popupFiles)) {
668 QUrl dest = u;
669 dest.setPath(Utils::concatPaths(dest.path(), KIO::encodeFileName(chosenFileName)));
670
671 QList<QUrl> lstSrc;
672 lstSrc.append(uSrc);
673 KIO::Job *kjob;
674 if (m_copyData.m_isSymlink) {
675 KIO::CopyJob *linkJob = KIO::linkAs(uSrc, dest);
676 kjob = linkJob;
678 } else if (src.startsWith(QLatin1String(":/"))) {
679 QFile srcFile(src);
680 if (!srcFile.open(QIODevice::ReadOnly)) {
681 return;
682 }
683 // The QFile won't live long enough for the job, so let's buffer the contents
684 const QByteArray srcBuf(srcFile.readAll());
685 KIO::StoredTransferJob *putJob = KIO::storedPut(srcBuf, dest, -1);
686 kjob = putJob;
688 } else {
689 // qDebug() << "KIO::copyAs(" << uSrc.url() << "," << dest.url() << ")";
690 KIO::CopyJob *job = KIO::copyAs(uSrc, dest);
691 job->setDefaultPermissions(true);
692 kjob = job;
694 }
695 KJobWidgets::setWindow(kjob, m_parentWidget);
697 }
698}
699
700void KNewFileMenuPrivate::executeUrlDesktopFile(const KNewFileMenuSingleton::Entry &entry)
701{
702 KNameAndUrlInputDialog *dlg = new KNameAndUrlInputDialog(i18n("Name for new link:"), entry.comment, m_popupFiles.first(), m_parentWidget);
703 m_copyData.m_templatePath = entry.templatePath;
704 dlg->setModal(q->isModal());
706 dlg->setWindowTitle(i18n("Create link to URL"));
707 m_fileDialog = dlg;
708 QObject::connect(dlg, &QDialog::accepted, q, [this]() {
709 slotUrlDesktopFile();
710 });
711 dlg->show();
712}
713
714void KNewFileMenuPrivate::fillMenu()
715{
716 QMenu *menu = q->menu();
717 menu->clear();
718 m_menuDev->menu()->clear();
719 m_newDirAction = nullptr;
720
721 std::set<QString> seenTexts;
722 QString lastTemplatePath;
723 // these shall be put at special positions
724 QAction *linkURL = nullptr;
725 QAction *linkApp = nullptr;
726 QAction *linkPath = nullptr;
727
728 KNewFileMenuSingleton *s = kNewMenuGlobals();
729 int idx = 0;
730 for (auto &entry : *s->templatesList) {
731 ++idx;
732 if (entry.entryType != KNewFileMenuSingleton::Unknown) {
733 // There might be a .desktop for that one already.
734
735 // In fact, we skip any second item that has the same text as another one.
736 // Duplicates in a menu look bad in any case.
737 const auto [it, isInserted] = seenTexts.insert(entry.text);
738 if (isInserted) {
739 // const KNewFileMenuSingleton::Entry entry = templatesList->at(i-1);
740
741 const QString templatePath = entry.templatePath;
742 // The best way to identify the "Create Directory", "Link to Location", "Link to Application" was the template
743 if (templatePath.endsWith(QLatin1String("emptydir"))) {
744 QAction *act = new QAction(q);
745 m_newDirAction = act;
746 act->setIcon(QIcon::fromTheme(entry.icon));
747 act->setText(i18nc("@item:inmenu Create New", "%1", entry.text));
748 act->setActionGroup(m_newMenuGroup);
749
750 // If there is a shortcut action copy its shortcut
751 if (m_newFolderShortcutAction) {
752 act->setShortcuts(m_newFolderShortcutAction->shortcuts());
753 // Both actions have now the same shortcut, so this will prevent the "Ambiguous shortcut detected" dialog.
755 // We also need to react to shortcut changes.
756 QObject::connect(m_newFolderShortcutAction, &QAction::changed, act, [act, this]() {
757 act->setShortcuts(m_newFolderShortcutAction->shortcuts());
758 });
759 }
760
761 menu->addAction(act);
762 menu->addSeparator();
763 } else {
764 if (lastTemplatePath.startsWith(QDir::homePath()) && !templatePath.startsWith(QDir::homePath())) {
765 menu->addSeparator();
766 }
767 if (!m_supportedMimeTypes.isEmpty()) {
768 bool keep = false;
769
770 // We need to do MIME type filtering, for real files.
771 const bool createSymlink = entry.templatePath == QLatin1String("__CREATE_SYMLINK__");
772 if (createSymlink) {
773 keep = true;
774 } else if (!KDesktopFile::isDesktopFile(entry.templatePath)) {
775 // Determine MIME type on demand
776 QMimeDatabase db;
777 QMimeType mime;
778 if (entry.mimeType.isEmpty()) {
779 mime = db.mimeTypeForFile(entry.templatePath);
780 // qDebug() << entry.templatePath << "is" << mime.name();
781 entry.mimeType = mime.name();
782 } else {
783 mime = db.mimeTypeForName(entry.mimeType);
784 }
785 for (const QString &supportedMime : std::as_const(m_supportedMimeTypes)) {
786 if (mime.inherits(supportedMime)) {
787 keep = true;
788 break;
789 }
790 }
791 }
792
793 if (!keep) {
794 // qDebug() << "Not keeping" << entry.templatePath;
795 continue;
796 }
797 }
798
799 QAction *act = new QAction(q);
800 act->setData(idx);
801 act->setIcon(QIcon::fromTheme(entry.icon));
802 act->setText(i18nc("@item:inmenu Create New", "%1", entry.text));
803 act->setActionGroup(m_newMenuGroup);
804
805 // qDebug() << templatePath << entry.filePath;
806
807 if (templatePath.endsWith(QLatin1String("/URL.desktop"))) {
808 linkURL = act;
809 } else if (templatePath.endsWith(QLatin1String("/Program.desktop"))) {
810 linkApp = act;
811 } else if (entry.filePath.endsWith(QLatin1String("/linkPath.desktop"))) {
812 linkPath = act;
813 } else if (KDesktopFile::isDesktopFile(templatePath)) {
814 KDesktopFile df(templatePath);
815 if (df.readType() == QLatin1String("FSDevice")) {
816 m_menuDev->menu()->addAction(act);
817 } else {
818 menu->addAction(act);
819 }
820 } else {
821 if (!m_firstFileEntry) {
822 m_firstFileEntry = &entry;
823
824 // If there is a shortcut action copy its shortcut
825 if (m_newFileShortcutAction) {
826 act->setShortcuts(m_newFileShortcutAction->shortcuts());
827 // Both actions have now the same shortcut, so this will prevent the "Ambiguous shortcut detected" dialog.
829 // We also need to react to shortcut changes.
830 QObject::connect(m_newFileShortcutAction, &QAction::changed, act, [act, this]() {
831 act->setShortcuts(m_newFileShortcutAction->shortcuts());
832 });
833 }
834 }
835 menu->addAction(act);
836 }
837 }
838 }
839 lastTemplatePath = entry.templatePath;
840 } else { // Separate system from personal templates
841 Q_ASSERT(entry.entryType != 0);
842 menu->addSeparator();
843 }
844 }
845
846 if (m_supportedMimeTypes.isEmpty()) {
847 menu->addSeparator();
848 if (linkURL) {
849 menu->addAction(linkURL);
850 }
851 if (linkPath) {
852 menu->addAction(linkPath);
853 }
854 if (linkApp) {
855 menu->addAction(linkApp);
856 }
857 Q_ASSERT(m_menuDev);
858 if (!m_menuDev->menu()->isEmpty()) {
859 menu->addAction(m_menuDev);
860 }
861 }
862}
863
864QUrl KNewFileMenuPrivate::mostLocalUrl(const QUrl &url)
865{
866 if (url.isLocalFile() || KProtocolInfo::protocolClass(url.scheme()) != QLatin1String(":local")) {
867 return url;
868 }
869
871 KJobWidgets::setWindow(job, m_parentWidget);
872
873 return job->exec() ? job->mostLocalUrl() : url;
874}
875
876void KNewFileMenuPrivate::slotAbortDialog()
877{
878 m_text = QString();
879 if (m_creatingDirectory) {
880 Q_EMIT q->directoryCreationRejected(m_baseUrl);
881 } else {
882 Q_EMIT q->fileCreationRejected(m_baseUrl);
883 }
884}
885
886void KNewFileMenuPrivate::slotActionTriggered(QAction *action)
887{
888 q->trigger(); // was for kdesktop's slotNewMenuActivated() in kde3 times. Can't hurt to keep it...
889
890 if (action == m_newDirAction) {
891 q->createDirectory();
892 return;
893 }
894 const int id = action->data().toInt();
895 Q_ASSERT(id > 0);
896
897 KNewFileMenuSingleton *s = kNewMenuGlobals();
898 const KNewFileMenuSingleton::Entry entry = s->templatesList->at(id - 1);
899
900 const bool createSymlink = entry.templatePath == QLatin1String("__CREATE_SYMLINK__");
901
902 m_copyData = KNewFileMenuCopyData();
903
904 if (createSymlink) {
905 m_copyData.m_isSymlink = true;
906 executeSymLink(entry);
907 } else if (KDesktopFile::isDesktopFile(entry.templatePath)) {
908 KDesktopFile df(entry.templatePath);
909 if (df.readType() == QLatin1String("Link")) {
910 executeUrlDesktopFile(entry);
911 } else { // any other desktop file (Device, App, etc.)
912 executeOtherDesktopFile(entry);
913 }
914 } else {
915 executeRealFileOrDir(entry);
916 }
917}
918
919void KNewFileMenuPrivate::slotCreateDirectory()
920{
921 // Automatically trim trailing spaces since they're pretty much always
922 // unintentional and can cause issues on Windows in shared environments
923 while (m_text.endsWith(QLatin1Char(' '))) {
924 m_text.chop(1);
925 }
926
927 QUrl url;
928 QUrl baseUrl = m_popupFiles.first();
929
930 QString name = expandTilde(m_text);
931
932 if (!name.isEmpty()) {
933 if (Utils::isAbsoluteLocalPath(name)) {
934 url = QUrl::fromLocalFile(name);
935 } else {
936 url = baseUrl;
937 url.setPath(Utils::concatPaths(url.path(), name));
938 }
939 }
940
941 KIO::Job *job;
942 if (name.contains(QLatin1Char('/'))) {
943 // If the name contains any slashes, use mkpath so that a/b/c works.
944 job = KIO::mkpath(url, baseUrl);
946 } else {
947 // If not, use mkdir so it will fail if the name of an existing folder was used
948 job = KIO::mkdir(url);
949 KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Mkdir, QList<QUrl>(), url, job);
950 }
951 job->setProperty("newDirectoryURL", url);
953 KJobWidgets::setWindow(job, m_parentWidget);
954
955 if (job) {
956 // We want the error handling to be done by slotResult so that subclasses can reimplement it
959 }
960 slotAbortDialog();
961}
962
963static QStringList getInstalledTemplates()
964{
967 static bool templateWarningShown = false;
968 // Some distros set TemplatesLocation to home dir, which means it hasn't been set up and should be ignored
969 // Otherwise everything in the home folder will be used as a template
970 if (templateFolder != QDir::homePath()) {
971 list << templateFolder;
972 } else if (!templateWarningShown) {
973 qCWarning(KFILEWIDGETS_LOG) << "Your 'templates' folder is set to your home folder. "
974 "This is probably an error in your settings. Ignoring it. "
975 "You can change the setting by running `systemsettings kcm_desktoppaths`. ";
976 templateWarningShown = true;
977 }
978 return list;
979}
980
981static QStringList getTemplateFilePaths(const QStringList &templates)
982{
983 QDir dir;
984 QStringList files;
985 for (const QString &path : templates) {
986 dir.setPath(path);
987 const QStringList entryList = dir.entryList(QDir::NoDotAndDotDot | QDir::AllEntries);
988 files.reserve(files.size() + entryList.size());
989 for (const QString &entry : entryList) {
990 const QString file = Utils::concatPaths(dir.path(), entry);
991 files.append(file);
992 }
993 }
994 return files;
995}
996
997void KNewFileMenuPrivate::slotFillTemplates()
998{
999 KNewFileMenuSingleton *instance = kNewMenuGlobals();
1000 // qDebug();
1001
1002 const QStringList installedTemplates = getInstalledTemplates();
1003 const QStringList qrcTemplates{QStringLiteral(":/kio5/newfile-templates")};
1004 const QStringList templates = qrcTemplates + installedTemplates;
1005
1006 // Ensure any changes in the templates dir will call this
1007 if (!instance->dirWatch) {
1008 instance->dirWatch = std::make_unique<KDirWatch>();
1009 for (const QString &dir : installedTemplates) {
1010 instance->dirWatch->addDir(dir);
1011 }
1012
1013 auto slotFunc = [this]() {
1014 slotFillTemplates();
1015 };
1016 QObject::connect(instance->dirWatch.get(), &KDirWatch::dirty, q, slotFunc);
1017 QObject::connect(instance->dirWatch.get(), &KDirWatch::created, q, slotFunc);
1018 QObject::connect(instance->dirWatch.get(), &KDirWatch::deleted, q, slotFunc);
1019 // Ok, this doesn't cope with new dirs in XDG_DATA_DIRS, but that's another story
1020 }
1021
1022 // Look into "templates" dirs.
1023 QStringList files = getTemplateFilePaths(templates);
1024
1025 // Remove files that begin with a dot.
1026 // dir.entryList(QDir::NoDotAndDotDot | QDir::AllEntries) does not disregard internal files that
1027 // start with a dot like :/kio5/newfile-templates/.source
1028 auto removeFunc = [](const QString &path) {
1029 QFileInfo fileinfo(path);
1030 return fileinfo.fileName().startsWith(QLatin1Char('.'));
1031 };
1032 files.erase(std::remove_if(files.begin(), files.end(), removeFunc), files.end());
1033
1034 // Ensure desktop files are always before template files
1035 // This ensures consistent behavior
1036 std::partition(files.begin(), files.end(), [](const QString &a) {
1037 return a.endsWith(QStringLiteral(".desktop"));
1038 });
1039
1040 std::vector<EntryInfo> uniqueEntries;
1041 QMimeDatabase db;
1042 for (const QString &file : files) {
1043 // qDebug() << file;
1044 KNewFileMenuSingleton::Entry entry;
1045 entry.entryType = KNewFileMenuSingleton::Unknown; // not parsed yet
1046 QString url;
1047 QString key;
1048
1049 if (file.endsWith(QLatin1String(".desktop"))) {
1050 entry.filePath = file;
1051 const KDesktopFile config(file);
1052 url = config.desktopGroup().readEntry("URL");
1053 key = config.desktopGroup().readEntry("Name");
1054 }
1055 // Preparse non-.desktop files
1056 else {
1057 QFileInfo fileinfo(file);
1058 url = file;
1059 key = fileinfo.fileName();
1060 entry.entryType = KNewFileMenuSingleton::Template;
1061 entry.text = fileinfo.baseName();
1062 entry.filePath = fileinfo.completeBaseName();
1063 entry.templatePath = file;
1064 QMimeType mime = db.mimeTypeForFile(file);
1065 entry.mimeType = mime.name();
1066 entry.icon = mime.iconName();
1067 entry.comment = i18nc("@label:textbox Prompt for new file of type", "Enter %1 filename:", mime.comment());
1068 }
1069 // Put Directory first in the list (a bit hacky),
1070 // and TextFile before others because it's the most used one.
1071 // This also sorts by user-visible name.
1072 // The rest of the re-ordering is done in fillMenu.
1073 if (file.endsWith(QLatin1String("Directory.desktop"))) {
1074 key.prepend(QLatin1Char('0'));
1075 } else if (file.startsWith(QDir::homePath())) {
1076 key.prepend(QLatin1Char('1'));
1077 } else if (file.endsWith(QLatin1String("TextFile.desktop"))) {
1078 key.prepend(QLatin1Char('2'));
1079 } else {
1080 key.prepend(QLatin1Char('3'));
1081 }
1082
1083 EntryInfo eInfo = {key, url, entry};
1084 auto it = std::find_if(uniqueEntries.begin(), uniqueEntries.end(), [&url](const EntryInfo &info) {
1085 return url == info.url;
1086 });
1087
1088 if (it != uniqueEntries.cend()) {
1089 *it = eInfo;
1090 } else {
1091 uniqueEntries.push_back(eInfo);
1092 }
1093 }
1094
1095 std::sort(uniqueEntries.begin(), uniqueEntries.end(), [](const EntryInfo &a, const EntryInfo &b) {
1096 return a.key < b.key;
1097 });
1098
1099 ++instance->templatesVersion;
1100 instance->filesParsed = false;
1101
1102 instance->templatesList->clear();
1103
1104 instance->templatesList->reserve(uniqueEntries.size());
1105 for (const auto &info : uniqueEntries) {
1106 instance->templatesList->append(info.entry);
1107 };
1108}
1109
1110void KNewFileMenuPrivate::_k_slotOtherDesktopFile(KPropertiesDialog *sender)
1111{
1112 // The properties dialog took care of the copying, so we're done
1113 Q_EMIT q->fileCreated(sender->url());
1114}
1115
1116void KNewFileMenuPrivate::slotOtherDesktopFileClosed()
1117{
1118 QFile::remove(m_tempFileToDelete);
1119}
1120
1121void KNewFileMenuPrivate::slotRealFileOrDir()
1122{
1123 // Automatically trim trailing spaces since they're pretty much always
1124 // unintentional and can cause issues on Windows in shared environments
1125 while (m_text.endsWith(QLatin1Char(' '))) {
1126 m_text.chop(1);
1127 }
1128 m_copyData.m_chosenFileName = m_text;
1129 slotAbortDialog();
1130 executeStrategy();
1131}
1132
1133void KNewFileMenuPrivate::slotSymLink()
1134{
1135 KNameAndUrlInputDialog *dlg = static_cast<KNameAndUrlInputDialog *>(m_fileDialog);
1136
1137 m_copyData.m_chosenFileName = dlg->name(); // no path
1138 const QString linkTarget = dlg->urlText();
1139
1140 if (m_copyData.m_chosenFileName.isEmpty() || linkTarget.isEmpty()) {
1141 return;
1142 }
1143
1144 m_copyData.m_src = linkTarget;
1145 executeStrategy();
1146}
1147
1148void KNewFileMenuPrivate::_k_delayedSlotTextChanged()
1149{
1150 m_delayedSlotTextChangedTimer->start();
1151 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!m_lineEdit->text().isEmpty());
1152}
1153
1154void KNewFileMenuPrivate::_k_slotTextChanged(const QString &text)
1155{
1156 // Validate input, displaying a KMessageWidget for questionable names
1157
1158 if (text.isEmpty()) {
1159 m_messageWidget->hide();
1160 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1161 }
1162
1163 // Don't allow creating folders that would mask . or ..
1164 else if (text == QLatin1Char('.') || text == QLatin1String("..")) {
1165 m_messageWidget->setText(
1166 xi18nc("@info", "The name <filename>%1</filename> cannot be used because it is reserved for use by the operating system.", text));
1167 m_messageWidget->setMessageType(KMessageWidget::Error);
1168 m_messageWidget->animatedShow();
1169 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1170 }
1171
1172 // File or folder would be hidden; show warning
1173 else if (text.startsWith(QLatin1Char('.'))) {
1174 m_messageWidget->setText(xi18nc("@info", "The name <filename>%1</filename> starts with a dot, so it will be hidden by default.", text));
1175 m_messageWidget->setMessageType(KMessageWidget::Warning);
1176 m_messageWidget->animatedShow();
1177 }
1178
1179 // File or folder begins with a space; show warning
1180 else if (text.startsWith(QLatin1Char(' '))) {
1181 m_messageWidget->setText(xi18nc("@info",
1182 "The name <filename>%1</filename> starts with a space, which will result in it being shown before other items when "
1183 "sorting alphabetically, among other potential oddities.",
1184 text));
1185 m_messageWidget->setMessageType(KMessageWidget::Warning);
1186 m_messageWidget->animatedShow();
1187 }
1188#ifndef Q_OS_WIN
1189 // Inform the user that slashes in folder names create a directory tree
1190 else if (text.contains(QLatin1Char('/'))) {
1191 if (m_creatingDirectory) {
1192 QStringList folders = text.split(QLatin1Char('/'));
1193 if (!folders.isEmpty()) {
1194 if (folders.first().isEmpty()) {
1195 folders.removeFirst();
1196 }
1197 }
1198 QString label;
1199 if (folders.count() > 1) {
1200 label = i18n("Using slashes in folder names will create sub-folders, like so:");
1201 QString indentation = QString();
1202 for (const QString &folder : std::as_const(folders)) {
1203 label.append(QLatin1Char('\n'));
1204 label.append(indentation);
1205 label.append(folder);
1206 label.append(QStringLiteral("/"));
1207 indentation.append(QStringLiteral(" "));
1208 }
1209 } else {
1210 label = i18n("Using slashes in folder names will create sub-folders.");
1211 }
1212 m_messageWidget->setText(label);
1214 m_messageWidget->animatedShow();
1215 }
1216 }
1217#endif
1218
1219#ifdef Q_OS_WIN
1220 // Slashes and backslashes are not allowed in Windows filenames; show error
1221 else if (text.contains(QLatin1Char('/'))) {
1222 m_messageWidget->setText(i18n("Slashes cannot be used in file and folder names."));
1223 m_messageWidget->setMessageType(KMessageWidget::Error);
1224 m_messageWidget->animatedShow();
1225 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1226 } else if (text.contains(QLatin1Char('\\'))) {
1227 m_messageWidget->setText(i18n("Backslashes cannot be used in file and folder names."));
1228 m_messageWidget->setMessageType(KMessageWidget::Error);
1229 m_messageWidget->animatedShow();
1230 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1231 }
1232#endif
1233
1234 // Using a tilde to begin a file or folder name is not recommended
1235 else if (text.startsWith(QLatin1Char('~'))) {
1236 m_messageWidget->setText(
1237 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 "
1238 "things."));
1239 m_messageWidget->setMessageType(KMessageWidget::Warning);
1240 m_messageWidget->animatedShow();
1241 } else {
1242 m_messageWidget->hide();
1243 }
1244
1245 if (!text.isEmpty()) {
1246 // Check file does not already exists
1247 m_statRunning = true;
1248 QUrl url;
1249 if (m_creatingDirectory && text.at(0) == QLatin1Char('~')) {
1251 } else {
1252 url = QUrl(m_baseUrl.toString() + QLatin1Char('/') + text);
1253 }
1254 KIO::StatJob *job = KIO::stat(url, KIO::StatJob::StatSide::DestinationSide, KIO::StatDetail::StatBasic, KIO::HideProgressInfo);
1255 QObject::connect(job, &KJob::result, m_fileDialog, [this](KJob *job) {
1256 _k_slotStatResult(job);
1257 });
1258 job->start();
1259 }
1260
1261 m_text = text;
1262}
1263
1264void KNewFileMenu::setSelectDirWhenAlreadyExist(bool shouldSelectExistingDir)
1265{
1266 d->m_selectDirWhenAlreadyExists = shouldSelectExistingDir;
1267}
1268
1269void KNewFileMenuPrivate::_k_slotStatResult(KJob *job)
1270{
1271 m_statRunning = false;
1272 KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
1273 // ignore stat Result when the lineEdit has changed
1274 const QUrl url = statJob->url().adjusted(QUrl::StripTrailingSlash);
1275 if (m_creatingDirectory && m_lineEdit->text().startsWith(QLatin1Char('~'))) {
1276 if (url.path() != KShell::tildeExpand(m_lineEdit->text())) {
1277 return;
1278 }
1279 } else if (url.fileName() != m_lineEdit->text()) {
1280 return;
1281 }
1282 bool accepted = m_acceptedPressed;
1283 m_acceptedPressed = false;
1284 auto error = job->error();
1285 if (error) {
1286 if (error == KIO::ERR_DOES_NOT_EXIST) {
1287 // fine for file creation
1288 if (accepted) {
1289 m_fileDialog->accept();
1290 }
1291 } else {
1292 qWarning() << error << job->errorString();
1293 }
1294 } else {
1295 bool shouldEnable = false;
1297
1298 const KIO::UDSEntry &entry = statJob->statResult();
1299 if (entry.isDir()) {
1300 if (m_selectDirWhenAlreadyExists && m_creatingDirectory) {
1301 // allow "overwrite" of dir
1302 messageType = KMessageWidget::Information;
1303 shouldEnable = true;
1304 }
1305 m_messageWidget->setText(xi18nc("@info", "A directory with name <filename>%1</filename> already exists.", m_text));
1306 } else {
1307 m_messageWidget->setText(xi18nc("@info", "A file with name <filename>%1</filename> already exists.", m_text));
1308 }
1309 m_messageWidget->setMessageType(messageType);
1310 m_messageWidget->animatedShow();
1311 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(shouldEnable);
1312
1313 if (accepted && shouldEnable) {
1314 m_fileDialog->accept();
1315 }
1316 }
1317}
1318
1319void KNewFileMenuPrivate::slotUrlDesktopFile()
1320{
1321 KNameAndUrlInputDialog *dlg = static_cast<KNameAndUrlInputDialog *>(m_fileDialog);
1322 QString name = dlg->name();
1323 const QLatin1String ext(".desktop");
1324 if (!name.endsWith(ext)) {
1325 name += ext;
1326 }
1327 m_copyData.m_chosenFileName = name; // no path
1328 QUrl linkUrl = dlg->url();
1329
1330 // Filter user input so that short uri entries, e.g. www.kde.org, are
1331 // handled properly. This not only makes the icon detection below work
1332 // properly, but opening the URL link where the short uri will not be
1333 // sent to the application (opening such link Konqueror fails).
1334 KUriFilterData uriData;
1335 uriData.setData(linkUrl); // the url to put in the file
1336 uriData.setCheckForExecutables(false);
1337
1338 if (KUriFilter::self()->filterUri(uriData, QStringList{QStringLiteral("kshorturifilter")})) {
1339 linkUrl = uriData.uri();
1340 }
1341
1342 if (m_copyData.m_chosenFileName.isEmpty() || linkUrl.isEmpty()) {
1343 return;
1344 }
1345
1346 // It's a "URL" desktop file; we need to make a temp copy of it, to modify it
1347 // before copying it to the final destination [which could be a remote protocol]
1348 QTemporaryFile tmpFile;
1349 tmpFile.setAutoRemove(false); // done below
1350 if (!tmpFile.open()) {
1351 qCritical() << "Couldn't create temp file!";
1352 return;
1353 }
1354
1355 if (!checkSourceExists(m_copyData.m_templatePath)) {
1356 return;
1357 }
1358
1359 // First copy the template into the temp file
1360 QFile file(m_copyData.m_templatePath);
1361 if (!file.open(QIODevice::ReadOnly)) {
1362 qCritical() << "Couldn't open template" << m_copyData.m_templatePath;
1363 return;
1364 }
1365 const QByteArray data = file.readAll();
1366 tmpFile.write(data);
1367 const QString tempFileName = tmpFile.fileName();
1368 Q_ASSERT(!tempFileName.isEmpty());
1369 tmpFile.close();
1370 file.close();
1371
1372 KDesktopFile df(tempFileName);
1373 KConfigGroup group = df.desktopGroup();
1374
1375 if (linkUrl.isLocalFile()) {
1376 KFileItem fi(linkUrl);
1377 group.writeEntry("Icon", fi.iconName());
1378 } else {
1379 group.writeEntry("Icon", KProtocolInfo::icon(linkUrl.scheme()));
1380 }
1381
1382 group.writePathEntry("URL", linkUrl.toDisplayString());
1383 group.writeEntry("Name", dlg->name()); // Used as user-visible name by kio_desktop
1384 df.sync();
1385
1386 m_copyData.m_src = tempFileName;
1387 m_copyData.m_tempFileToDelete = tempFileName;
1388
1389 executeStrategy();
1390}
1391
1393 : KActionMenu(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Create New"), parent)
1394 , d(std::make_unique<KNewFileMenuPrivate>(this))
1395{
1396 // Don't fill the menu yet
1397 // We'll do that in checkUpToDate (should be connected to aboutToShow)
1398 d->m_newMenuGroup = new QActionGroup(this);
1399 connect(d->m_newMenuGroup, &QActionGroup::triggered, this, [this](QAction *action) {
1400 d->slotActionTriggered(action);
1401 });
1402
1403 // Connect directory creation signals
1404 connect(this, &KNewFileMenu::directoryCreationStarted, this, [this] {
1405 d->m_isCreateDirectoryRunning = true;
1406 });
1407 connect(this, &KNewFileMenu::directoryCreated, this, [this] {
1408 d->m_isCreateDirectoryRunning = false;
1409 });
1411 d->m_isCreateDirectoryRunning = false;
1412 });
1413
1414 // Connect file creation signals
1415 connect(this, &KNewFileMenu::fileCreationStarted, this, [this] {
1416 d->m_isCreateFileRunning = true;
1417 });
1418 connect(this, &KNewFileMenu::fileCreated, this, [this] {
1419 d->m_isCreateFileRunning = false;
1420 });
1421 connect(this, &KNewFileMenu::fileCreationRejected, this, [this] {
1422 d->m_isCreateFileRunning = false;
1423 });
1424
1425 d->m_parentWidget = qobject_cast<QWidget *>(parent);
1426 d->m_newDirAction = nullptr;
1427
1428 d->m_menuDev = new KActionMenu(QIcon::fromTheme(QStringLiteral("drive-removable-media")), i18n("Link to Device"), this);
1429}
1430
1431KNewFileMenu::~KNewFileMenu() = default;
1432
1434{
1435 KNewFileMenuSingleton *s = kNewMenuGlobals();
1436 // qDebug() << this << "m_menuItemsVersion=" << d->m_menuItemsVersion
1437 // << "s->templatesVersion=" << s->templatesVersion;
1438 if (d->m_menuItemsVersion < s->templatesVersion || s->templatesVersion == 0) {
1439 // qDebug() << "recreating actions";
1440 // We need to clean up the action collection
1441 // We look for our actions using the group
1442 qDeleteAll(d->m_newMenuGroup->actions());
1443
1444 if (!s->templatesList) { // No templates list up to now
1445 s->templatesList = new KNewFileMenuSingleton::EntryList;
1446 d->slotFillTemplates();
1447 s->parseFiles();
1448 }
1449
1450 // This might have been already done for other popupmenus,
1451 // that's the point in s->filesParsed.
1452 if (!s->filesParsed) {
1453 s->parseFiles();
1454 }
1455
1456 d->fillMenu();
1457
1458 d->m_menuItemsVersion = s->templatesVersion;
1459 }
1460}
1461
1463{
1464 if (d->m_popupFiles.isEmpty()) {
1465 return;
1466 }
1467
1468 d->m_baseUrl = d->m_popupFiles.first();
1469
1470 if (d->m_isCreateDirectoryRunning) {
1471 qCWarning(KFILEWIDGETS_LOG) << "Directory creation is already running for " << d->m_baseUrl;
1472 }
1473
1474 QString name = !d->m_text.isEmpty() ? d->m_text : i18nc("Default name for a new folder", "New Folder");
1475
1476 auto nameJob = new KIO::NameFinderJob(d->m_baseUrl, name, this);
1477 connect(nameJob, &KJob::result, this, [nameJob, name, this]() mutable {
1478 if (!nameJob->error()) {
1479 d->m_baseUrl = nameJob->baseUrl();
1480 name = nameJob->finalName();
1481 }
1482 d->showNewDirNameDlg(name);
1483 });
1484 nameJob->start();
1485 Q_EMIT directoryCreationStarted(d->m_baseUrl);
1486}
1487
1489{
1490 return d->m_isCreateDirectoryRunning;
1491}
1492
1493void KNewFileMenuPrivate::showNewDirNameDlg(const QString &name)
1494{
1495 initDialog();
1496
1497 m_fileDialog->setWindowTitle(i18nc("@title:window", "Create New Folder"));
1498
1499 m_label->setText(i18n("Create new folder in %1:", m_baseUrl.toDisplayString(QUrl::PreferLocalFile)));
1500
1501 m_lineEdit->setText(name);
1502
1503 m_creatingDirectory = true;
1504 _k_slotTextChanged(name); // have to save string in m_text in case user does not touch dialog
1505 QObject::connect(m_lineEdit, &QLineEdit::textChanged, q, [this]() {
1506 _k_delayedSlotTextChanged();
1507 });
1508 m_delayedSlotTextChangedTimer->callOnTimeout(m_lineEdit, [this]() {
1509 _k_slotTextChanged(m_lineEdit->text());
1510 });
1511
1512 QObject::connect(m_fileDialog, &QDialog::accepted, q, [this]() {
1513 slotCreateDirectory();
1514 });
1515 QObject::connect(m_fileDialog, &QDialog::rejected, q, [this]() {
1516 slotAbortDialog();
1517 });
1518
1519 m_fileDialog->show();
1520 m_lineEdit->selectAll();
1521 m_lineEdit->setFocus();
1522}
1523
1525{
1526 if (d->m_popupFiles.isEmpty()) {
1528 return;
1529 }
1530
1531 checkUpToDate();
1532 if (!d->m_firstFileEntry) {
1534 return;
1535 }
1536
1537 if (!d->m_isCreateFileRunning) {
1538 d->executeRealFileOrDir(*d->m_firstFileEntry);
1539 } else {
1540 qCWarning(KFILEWIDGETS_LOG) << "File creation is already running for " << d->m_firstFileEntry;
1541 }
1542}
1543
1545{
1546 return d->m_isCreateFileRunning;
1547}
1548
1550{
1551 return d->m_modal;
1552}
1553
1555{
1556 d->m_modal = modal;
1557}
1558
1560{
1561 d->m_parentWidget = parentWidget;
1562}
1563
1565{
1566 d->m_supportedMimeTypes = mime;
1567}
1568
1570{
1571 if (job->error()) {
1572 if (job->error() == KIO::ERR_DIR_ALREADY_EXIST && d->m_selectDirWhenAlreadyExists) {
1573 auto *simpleJob = ::qobject_cast<KIO::SimpleJob *>(job);
1574 if (simpleJob) {
1575 const QUrl jobUrl = simpleJob->url();
1576 // Select the existing dir
1577 Q_EMIT selectExistingDir(jobUrl);
1578 }
1579 } else { // All other errors
1580 static_cast<KIO::Job *>(job)->uiDelegate()->showErrorMessage();
1581 }
1582 } else {
1583 // Was this a copy or a mkdir?
1584 if (job->property("newDirectoryURL").isValid()) {
1585 QUrl newDirectoryURL = job->property("newDirectoryURL").toUrl();
1586 Q_EMIT directoryCreated(newDirectoryURL);
1587 } else {
1589 if (copyJob) {
1590 const QUrl destUrl = copyJob->destUrl();
1591 const QUrl localUrl = d->mostLocalUrl(destUrl);
1592 if (localUrl.isLocalFile()) {
1593 // Normal (local) file. Need to "touch" it, kio_file copied the mtime.
1594 (void)::utime(QFile::encodeName(localUrl.toLocalFile()).constData(), nullptr);
1595 }
1596 Q_EMIT fileCreated(destUrl);
1597 } else if (KIO::SimpleJob *simpleJob = ::qobject_cast<KIO::SimpleJob *>(job)) {
1598 // Called in the storedPut() case
1599#ifdef WITH_QTDBUS
1600 org::kde::KDirNotify::emitFilesAdded(simpleJob->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
1601#endif
1602 Q_EMIT fileCreated(simpleJob->url());
1603 }
1604 }
1605 }
1606 if (!d->m_tempFileToDelete.isEmpty()) {
1607 QFile::remove(d->m_tempFileToDelete);
1608 }
1609}
1610
1612{
1613 return d->m_supportedMimeTypes;
1614}
1615
1617{
1618 d->m_popupFiles = {directory};
1619
1620 if (directory.isEmpty()) {
1621 d->m_newMenuGroup->setEnabled(false);
1622 } else {
1623 if (KProtocolManager::supportsWriting(directory)) {
1624 d->m_newMenuGroup->setEnabled(true);
1625 if (d->m_newDirAction) {
1626 d->m_newDirAction->setEnabled(KProtocolManager::supportsMakeDir(directory)); // e.g. trash:/
1627 }
1628 } else {
1629 d->m_newMenuGroup->setEnabled(true);
1630 }
1631 }
1632}
1633
1635{
1636 return d->m_popupFiles.isEmpty() ? QUrl() : d->m_popupFiles.first();
1637}
1638
1640{
1641 d->m_newFolderShortcutAction = action;
1642}
1643
1645{
1646 d->m_newFileShortcutAction = action;
1647}
1648
1649#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)
A KFileItem is a generic class to handle a file, local or remote.
Definition kfileitem.h:36
CopyJob is used to move, copy or symlink files and directories.
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.
NameFinderJob finds a valid "New Folder" name.
A simple job (one url and one command).
const QUrl & url() const
Returns the SimpleJob's URL.
Definition simplejob.cpp:70
A KIO job that retrieves information about a file or directory.
const UDSEntry & statResult() const
Result of the stat operation.
Definition statjob.cpp:80
QUrl mostLocalUrl() const
most local URL
Definition statjob.cpp:85
StoredTransferJob is a TransferJob (for downloading or uploading data) that also stores a QByteArray ...
Universal Directory Service.
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 setCloseButtonVisible(bool visible)
void animatedShow()
void setMessageType(KMessageWidget::MessageType type)
void setWordWrap(bool wordWrap)
void setText(const QString &text)
Dialog to ask for a name (e.g. filename) and a URL Basically a merge of KLineEditDlg and KUrlRequeste...
The 'Create New' submenu, for creating files using templates (e.g. "new HTML file") and directories.
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.
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.
This class is a basic messaging class used to exchange filtering information between the filter plugi...
Definition kurifilter.h:153
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
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 name(StandardAction id)
QString label(StandardShortcut id)
QWidget * parentWidget() const const
void changed()
QVariant data() const const
void setIcon(const QIcon &icon)
QMenu * menu() const const
void setActionGroup(QActionGroup *group)
void setData(const QVariant &data)
void setShortcuts(QKeySequence::StandardKey key)
void setShortcutContext(Qt::ShortcutContext context)
QList< QKeySequence > shortcuts() const const
void setText(const QString &text)
void trigger()
const char * constData() const const
virtual void accept()
void accepted()
void finished(int result)
void setModal(bool modal)
virtual void reject()
void rejected()
QPushButton * button(StandardButton which) const const
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 setClearButtonEnabled(bool enable)
void selectAll()
void setSelection(int start, int length)
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()
bool isEmpty() const const
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
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
QStringView left(qsizetype length) const const
WidgetShortcut
WA_DeleteOnClose
QTemporaryFile * createNativeFile(QFile &file)
virtual QString fileName() const const override
void setAutoRemove(bool b)
QMetaObject::Connection callOnTimeout(Functor &&slot)
void setInterval(int msec)
bool isActive() const const
void setSingleShot(bool singleShot)
void start()
void stop()
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 toString(FormattingOptions options) const const
QString url(FormattingOptions options) const const
bool isValid() const const
int toInt(bool *ok) const const
QUrl toUrl() const const
void setEnabled(bool)
void hide()
void setMinimumWidth(int minw)
void setAttribute(Qt::WidgetAttribute attribute, bool on)
void setFocus()
void show()
void setSizePolicy(QSizePolicy)
void setWindowTitle(const QString &)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Sat Dec 21 2024 16:58:45 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.