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 list << templateFolder;
968 return list;
969}
970
971static QStringList getTemplateFilePaths(const QStringList &templates)
972{
973 QDir dir;
974 QStringList files;
975 for (const QString &path : templates) {
976 dir.setPath(path);
977 const QStringList entryList = dir.entryList(QDir::NoDotAndDotDot | QDir::AllEntries);
978 files.reserve(files.size() + entryList.size());
979 for (const QString &entry : entryList) {
980 const QString file = Utils::concatPaths(dir.path(), entry);
981 files.append(file);
982 }
983 }
984 return files;
985}
986
987void KNewFileMenuPrivate::slotFillTemplates()
988{
989 KNewFileMenuSingleton *instance = kNewMenuGlobals();
990 // qDebug();
991
992 const QStringList installedTemplates = getInstalledTemplates();
993 const QStringList qrcTemplates{QStringLiteral(":/kio5/newfile-templates")};
994 const QStringList templates = qrcTemplates + installedTemplates;
995
996 // Ensure any changes in the templates dir will call this
997 if (!instance->dirWatch) {
998 instance->dirWatch = std::make_unique<KDirWatch>();
999 for (const QString &dir : installedTemplates) {
1000 instance->dirWatch->addDir(dir);
1001 }
1002
1003 auto slotFunc = [this]() {
1004 slotFillTemplates();
1005 };
1006 QObject::connect(instance->dirWatch.get(), &KDirWatch::dirty, q, slotFunc);
1007 QObject::connect(instance->dirWatch.get(), &KDirWatch::created, q, slotFunc);
1008 QObject::connect(instance->dirWatch.get(), &KDirWatch::deleted, q, slotFunc);
1009 // Ok, this doesn't cope with new dirs in XDG_DATA_DIRS, but that's another story
1010 }
1011
1012 // Look into "templates" dirs.
1013 QStringList files = getTemplateFilePaths(templates);
1014
1015 // Remove files that begin with a dot.
1016 // dir.entryList(QDir::NoDotAndDotDot | QDir::AllEntries) does not disregard internal files that
1017 // start with a dot like :/kio5/newfile-templates/.source
1018 auto removeFunc = [](const QString &path) {
1019 QFileInfo fileinfo(path);
1020 return fileinfo.fileName().startsWith(QLatin1Char('.'));
1021 };
1022 files.erase(std::remove_if(files.begin(), files.end(), removeFunc), files.end());
1023
1024 // Ensure desktop files are always before template files
1025 // This ensures consistent behavior
1026 std::partition(files.begin(), files.end(), [](const QString &a) {
1027 return a.endsWith(QStringLiteral(".desktop"));
1028 });
1029
1030 std::vector<EntryInfo> uniqueEntries;
1031 QMimeDatabase db;
1032 for (const QString &file : files) {
1033 // qDebug() << file;
1034 KNewFileMenuSingleton::Entry entry;
1035 entry.entryType = KNewFileMenuSingleton::Unknown; // not parsed yet
1036 QString url;
1037 QString key;
1038
1039 if (file.endsWith(QLatin1String(".desktop"))) {
1040 entry.filePath = file;
1041 const KDesktopFile config(file);
1042 url = config.desktopGroup().readEntry("URL");
1043 key = config.desktopGroup().readEntry("Name");
1044 }
1045 // Preparse non-.desktop files
1046 else {
1047 QFileInfo fileinfo(file);
1048 url = file;
1049 key = fileinfo.fileName();
1050 entry.entryType = KNewFileMenuSingleton::Template;
1051 entry.text = fileinfo.baseName();
1052 entry.filePath = fileinfo.completeBaseName();
1053 entry.templatePath = file;
1054 QMimeType mime = db.mimeTypeForFile(file);
1055 entry.mimeType = mime.name();
1056 entry.icon = mime.iconName();
1057 entry.comment = i18nc("@label:textbox Prompt for new file of type", "Enter %1 filename:", mime.comment());
1058 }
1059 // Put Directory first in the list (a bit hacky),
1060 // and TextFile before others because it's the most used one.
1061 // This also sorts by user-visible name.
1062 // The rest of the re-ordering is done in fillMenu.
1063 if (file.endsWith(QLatin1String("Directory.desktop"))) {
1064 key.prepend(QLatin1Char('0'));
1065 } else if (file.startsWith(QDir::homePath())) {
1066 key.prepend(QLatin1Char('1'));
1067 } else if (file.endsWith(QLatin1String("TextFile.desktop"))) {
1068 key.prepend(QLatin1Char('2'));
1069 } else {
1070 key.prepend(QLatin1Char('3'));
1071 }
1072
1073 EntryInfo eInfo = {key, url, entry};
1074 auto it = std::find_if(uniqueEntries.begin(), uniqueEntries.end(), [&url](const EntryInfo &info) {
1075 return url == info.url;
1076 });
1077
1078 if (it != uniqueEntries.cend()) {
1079 *it = eInfo;
1080 } else {
1081 uniqueEntries.push_back(eInfo);
1082 }
1083 }
1084
1085 std::sort(uniqueEntries.begin(), uniqueEntries.end(), [](const EntryInfo &a, const EntryInfo &b) {
1086 return a.key < b.key;
1087 });
1088
1089 ++instance->templatesVersion;
1090 instance->filesParsed = false;
1091
1092 instance->templatesList->clear();
1093
1094 instance->templatesList->reserve(uniqueEntries.size());
1095 for (const auto &info : uniqueEntries) {
1096 instance->templatesList->append(info.entry);
1097 };
1098}
1099
1100void KNewFileMenuPrivate::_k_slotOtherDesktopFile(KPropertiesDialog *sender)
1101{
1102 // The properties dialog took care of the copying, so we're done
1103 Q_EMIT q->fileCreated(sender->url());
1104}
1105
1106void KNewFileMenuPrivate::slotOtherDesktopFileClosed()
1107{
1108 QFile::remove(m_tempFileToDelete);
1109}
1110
1111void KNewFileMenuPrivate::slotRealFileOrDir()
1112{
1113 // Automatically trim trailing spaces since they're pretty much always
1114 // unintentional and can cause issues on Windows in shared environments
1115 while (m_text.endsWith(QLatin1Char(' '))) {
1116 m_text.chop(1);
1117 }
1118 m_copyData.m_chosenFileName = m_text;
1119 slotAbortDialog();
1120 executeStrategy();
1121}
1122
1123void KNewFileMenuPrivate::slotSymLink()
1124{
1125 KNameAndUrlInputDialog *dlg = static_cast<KNameAndUrlInputDialog *>(m_fileDialog);
1126
1127 m_copyData.m_chosenFileName = dlg->name(); // no path
1128 const QString linkTarget = dlg->urlText();
1129
1130 if (m_copyData.m_chosenFileName.isEmpty() || linkTarget.isEmpty()) {
1131 return;
1132 }
1133
1134 m_copyData.m_src = linkTarget;
1135 executeStrategy();
1136}
1137
1138void KNewFileMenuPrivate::_k_delayedSlotTextChanged()
1139{
1140 m_delayedSlotTextChangedTimer->start();
1141 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!m_lineEdit->text().isEmpty());
1142}
1143
1144void KNewFileMenuPrivate::_k_slotTextChanged(const QString &text)
1145{
1146 // Validate input, displaying a KMessageWidget for questionable names
1147
1148 if (text.isEmpty()) {
1149 m_messageWidget->hide();
1150 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1151 }
1152
1153 // Don't allow creating folders that would mask . or ..
1154 else if (text == QLatin1Char('.') || text == QLatin1String("..")) {
1155 m_messageWidget->setText(
1156 xi18nc("@info", "The name <filename>%1</filename> cannot be used because it is reserved for use by the operating system.", text));
1157 m_messageWidget->setMessageType(KMessageWidget::Error);
1158 m_messageWidget->animatedShow();
1159 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1160 }
1161
1162 // File or folder would be hidden; show warning
1163 else if (text.startsWith(QLatin1Char('.'))) {
1164 m_messageWidget->setText(xi18nc("@info", "The name <filename>%1</filename> starts with a dot, so it will be hidden by default.", text));
1165 m_messageWidget->setMessageType(KMessageWidget::Warning);
1166 m_messageWidget->animatedShow();
1167 }
1168
1169 // File or folder begins with a space; show warning
1170 else if (text.startsWith(QLatin1Char(' '))) {
1171 m_messageWidget->setText(xi18nc("@info",
1172 "The name <filename>%1</filename> starts with a space, which will result in it being shown before other items when "
1173 "sorting alphabetically, among other potential oddities.",
1174 text));
1175 m_messageWidget->setMessageType(KMessageWidget::Warning);
1176 m_messageWidget->animatedShow();
1177 }
1178#ifndef Q_OS_WIN
1179 // Inform the user that slashes in folder names create a directory tree
1180 else if (text.contains(QLatin1Char('/'))) {
1181 if (m_creatingDirectory) {
1182 QStringList folders = text.split(QLatin1Char('/'));
1183 if (!folders.isEmpty()) {
1184 if (folders.first().isEmpty()) {
1185 folders.removeFirst();
1186 }
1187 }
1188 QString label;
1189 if (folders.count() > 1) {
1190 label = i18n("Using slashes in folder names will create sub-folders, like so:");
1191 QString indentation = QString();
1192 for (const QString &folder : std::as_const(folders)) {
1193 label.append(QLatin1Char('\n'));
1194 label.append(indentation);
1195 label.append(folder);
1196 label.append(QStringLiteral("/"));
1197 indentation.append(QStringLiteral(" "));
1198 }
1199 } else {
1200 label = i18n("Using slashes in folder names will create sub-folders.");
1201 }
1202 m_messageWidget->setText(label);
1204 m_messageWidget->animatedShow();
1205 }
1206 }
1207#endif
1208
1209#ifdef Q_OS_WIN
1210 // Slashes and backslashes are not allowed in Windows filenames; show error
1211 else if (text.contains(QLatin1Char('/'))) {
1212 m_messageWidget->setText(i18n("Slashes cannot be used in file and folder names."));
1213 m_messageWidget->setMessageType(KMessageWidget::Error);
1214 m_messageWidget->animatedShow();
1215 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1216 } else if (text.contains(QLatin1Char('\\'))) {
1217 m_messageWidget->setText(i18n("Backslashes cannot be used in file and folder names."));
1218 m_messageWidget->setMessageType(KMessageWidget::Error);
1219 m_messageWidget->animatedShow();
1220 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
1221 }
1222#endif
1223
1224 // Using a tilde to begin a file or folder name is not recommended
1225 else if (text.startsWith(QLatin1Char('~'))) {
1226 m_messageWidget->setText(
1227 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 "
1228 "things."));
1229 m_messageWidget->setMessageType(KMessageWidget::Warning);
1230 m_messageWidget->animatedShow();
1231 } else {
1232 m_messageWidget->hide();
1233 }
1234
1235 if (!text.isEmpty()) {
1236 // Check file does not already exists
1237 m_statRunning = true;
1238 QUrl url;
1239 if (m_creatingDirectory && text.at(0) == QLatin1Char('~')) {
1241 } else {
1242 url = QUrl(m_baseUrl.toString() + QLatin1Char('/') + text);
1243 }
1244 KIO::StatJob *job = KIO::stat(url, KIO::StatJob::StatSide::DestinationSide, KIO::StatDetail::StatBasic, KIO::HideProgressInfo);
1245 QObject::connect(job, &KJob::result, m_fileDialog, [this](KJob *job) {
1246 _k_slotStatResult(job);
1247 });
1248 job->start();
1249 }
1250
1251 m_text = text;
1252}
1253
1254void KNewFileMenu::setSelectDirWhenAlreadyExist(bool shouldSelectExistingDir)
1255{
1256 d->m_selectDirWhenAlreadyExists = shouldSelectExistingDir;
1257}
1258
1259void KNewFileMenuPrivate::_k_slotStatResult(KJob *job)
1260{
1261 m_statRunning = false;
1262 KIO::StatJob *statJob = static_cast<KIO::StatJob *>(job);
1263 // ignore stat Result when the lineEdit has changed
1264 const QUrl url = statJob->url().adjusted(QUrl::StripTrailingSlash);
1265 if (m_creatingDirectory && m_lineEdit->text().startsWith(QLatin1Char('~'))) {
1266 if (url.path() != KShell::tildeExpand(m_lineEdit->text())) {
1267 return;
1268 }
1269 } else if (url.fileName() != m_lineEdit->text()) {
1270 return;
1271 }
1272 bool accepted = m_acceptedPressed;
1273 m_acceptedPressed = false;
1274 auto error = job->error();
1275 if (error) {
1276 if (error == KIO::ERR_DOES_NOT_EXIST) {
1277 // fine for file creation
1278 if (accepted) {
1279 m_fileDialog->accept();
1280 }
1281 } else {
1282 qWarning() << error << job->errorString();
1283 }
1284 } else {
1285 bool shouldEnable = false;
1287
1288 const KIO::UDSEntry &entry = statJob->statResult();
1289 if (entry.isDir()) {
1290 if (m_selectDirWhenAlreadyExists && m_creatingDirectory) {
1291 // allow "overwrite" of dir
1292 messageType = KMessageWidget::Information;
1293 shouldEnable = true;
1294 }
1295 m_messageWidget->setText(xi18nc("@info", "A directory with name <filename>%1</filename> already exists.", m_text));
1296 } else {
1297 m_messageWidget->setText(xi18nc("@info", "A file with name <filename>%1</filename> already exists.", m_text));
1298 }
1299 m_messageWidget->setMessageType(messageType);
1300 m_messageWidget->animatedShow();
1301 m_buttonBox->button(QDialogButtonBox::Ok)->setEnabled(shouldEnable);
1302
1303 if (accepted && shouldEnable) {
1304 m_fileDialog->accept();
1305 }
1306 }
1307}
1308
1309void KNewFileMenuPrivate::slotUrlDesktopFile()
1310{
1311 KNameAndUrlInputDialog *dlg = static_cast<KNameAndUrlInputDialog *>(m_fileDialog);
1312 QString name = dlg->name();
1313 const QLatin1String ext(".desktop");
1314 if (!name.endsWith(ext)) {
1315 name += ext;
1316 }
1317 m_copyData.m_chosenFileName = name; // no path
1318 QUrl linkUrl = dlg->url();
1319
1320 // Filter user input so that short uri entries, e.g. www.kde.org, are
1321 // handled properly. This not only makes the icon detection below work
1322 // properly, but opening the URL link where the short uri will not be
1323 // sent to the application (opening such link Konqueror fails).
1324 KUriFilterData uriData;
1325 uriData.setData(linkUrl); // the url to put in the file
1326 uriData.setCheckForExecutables(false);
1327
1328 if (KUriFilter::self()->filterUri(uriData, QStringList{QStringLiteral("kshorturifilter")})) {
1329 linkUrl = uriData.uri();
1330 }
1331
1332 if (m_copyData.m_chosenFileName.isEmpty() || linkUrl.isEmpty()) {
1333 return;
1334 }
1335
1336 // It's a "URL" desktop file; we need to make a temp copy of it, to modify it
1337 // before copying it to the final destination [which could be a remote protocol]
1338 QTemporaryFile tmpFile;
1339 tmpFile.setAutoRemove(false); // done below
1340 if (!tmpFile.open()) {
1341 qCritical() << "Couldn't create temp file!";
1342 return;
1343 }
1344
1345 if (!checkSourceExists(m_copyData.m_templatePath)) {
1346 return;
1347 }
1348
1349 // First copy the template into the temp file
1350 QFile file(m_copyData.m_templatePath);
1351 if (!file.open(QIODevice::ReadOnly)) {
1352 qCritical() << "Couldn't open template" << m_copyData.m_templatePath;
1353 return;
1354 }
1355 const QByteArray data = file.readAll();
1356 tmpFile.write(data);
1357 const QString tempFileName = tmpFile.fileName();
1358 Q_ASSERT(!tempFileName.isEmpty());
1359 tmpFile.close();
1360 file.close();
1361
1362 KDesktopFile df(tempFileName);
1363 KConfigGroup group = df.desktopGroup();
1364
1365 if (linkUrl.isLocalFile()) {
1366 KFileItem fi(linkUrl);
1367 group.writeEntry("Icon", fi.iconName());
1368 } else {
1369 group.writeEntry("Icon", KProtocolInfo::icon(linkUrl.scheme()));
1370 }
1371
1372 group.writePathEntry("URL", linkUrl.toDisplayString());
1373 group.writeEntry("Name", dlg->name()); // Used as user-visible name by kio_desktop
1374 df.sync();
1375
1376 m_copyData.m_src = tempFileName;
1377 m_copyData.m_tempFileToDelete = tempFileName;
1378
1379 executeStrategy();
1380}
1381
1383 : KActionMenu(QIcon::fromTheme(QStringLiteral("document-new")), i18n("Create New"), parent)
1384 , d(std::make_unique<KNewFileMenuPrivate>(this))
1385{
1386 // Don't fill the menu yet
1387 // We'll do that in checkUpToDate (should be connected to aboutToShow)
1388 d->m_newMenuGroup = new QActionGroup(this);
1389 connect(d->m_newMenuGroup, &QActionGroup::triggered, this, [this](QAction *action) {
1390 d->slotActionTriggered(action);
1391 });
1392
1393 // Connect directory creation signals
1394 connect(this, &KNewFileMenu::directoryCreationStarted, this, [this] {
1395 d->m_isCreateDirectoryRunning = true;
1396 });
1397 connect(this, &KNewFileMenu::directoryCreated, this, [this] {
1398 d->m_isCreateDirectoryRunning = false;
1399 });
1401 d->m_isCreateDirectoryRunning = false;
1402 });
1403
1404 // Connect file creation signals
1405 connect(this, &KNewFileMenu::fileCreationStarted, this, [this] {
1406 d->m_isCreateFileRunning = true;
1407 });
1408 connect(this, &KNewFileMenu::fileCreated, this, [this] {
1409 d->m_isCreateFileRunning = false;
1410 });
1411 connect(this, &KNewFileMenu::fileCreationRejected, this, [this] {
1412 d->m_isCreateFileRunning = false;
1413 });
1414
1415 d->m_parentWidget = qobject_cast<QWidget *>(parent);
1416 d->m_newDirAction = nullptr;
1417
1418 d->m_menuDev = new KActionMenu(QIcon::fromTheme(QStringLiteral("drive-removable-media")), i18n("Link to Device"), this);
1419}
1420
1421KNewFileMenu::~KNewFileMenu() = default;
1422
1424{
1425 KNewFileMenuSingleton *s = kNewMenuGlobals();
1426 // qDebug() << this << "m_menuItemsVersion=" << d->m_menuItemsVersion
1427 // << "s->templatesVersion=" << s->templatesVersion;
1428 if (d->m_menuItemsVersion < s->templatesVersion || s->templatesVersion == 0) {
1429 // qDebug() << "recreating actions";
1430 // We need to clean up the action collection
1431 // We look for our actions using the group
1432 qDeleteAll(d->m_newMenuGroup->actions());
1433
1434 if (!s->templatesList) { // No templates list up to now
1435 s->templatesList = new KNewFileMenuSingleton::EntryList;
1436 d->slotFillTemplates();
1437 s->parseFiles();
1438 }
1439
1440 // This might have been already done for other popupmenus,
1441 // that's the point in s->filesParsed.
1442 if (!s->filesParsed) {
1443 s->parseFiles();
1444 }
1445
1446 d->fillMenu();
1447
1448 d->m_menuItemsVersion = s->templatesVersion;
1449 }
1450}
1451
1453{
1454 if (d->m_popupFiles.isEmpty()) {
1455 return;
1456 }
1457
1458 d->m_baseUrl = d->m_popupFiles.first();
1459
1460 if (d->m_isCreateDirectoryRunning) {
1461 qCWarning(KFILEWIDGETS_LOG) << "Directory creation is already running for " << d->m_baseUrl;
1462 }
1463
1464 QString name = !d->m_text.isEmpty() ? d->m_text : i18nc("Default name for a new folder", "New Folder");
1465
1466 auto nameJob = new KIO::NameFinderJob(d->m_baseUrl, name, this);
1467 connect(nameJob, &KJob::result, this, [nameJob, name, this]() mutable {
1468 if (!nameJob->error()) {
1469 d->m_baseUrl = nameJob->baseUrl();
1470 name = nameJob->finalName();
1471 }
1472 d->showNewDirNameDlg(name);
1473 });
1474 nameJob->start();
1475 Q_EMIT directoryCreationStarted(d->m_baseUrl);
1476}
1477
1479{
1480 return d->m_isCreateDirectoryRunning;
1481}
1482
1483void KNewFileMenuPrivate::showNewDirNameDlg(const QString &name)
1484{
1485 initDialog();
1486
1487 m_fileDialog->setWindowTitle(i18nc("@title:window", "Create New Folder"));
1488
1489 m_label->setText(i18n("Create new folder in %1:", m_baseUrl.toDisplayString(QUrl::PreferLocalFile)));
1490
1491 m_lineEdit->setText(name);
1492
1493 m_creatingDirectory = true;
1494 _k_slotTextChanged(name); // have to save string in m_text in case user does not touch dialog
1495 QObject::connect(m_lineEdit, &QLineEdit::textChanged, q, [this]() {
1496 _k_delayedSlotTextChanged();
1497 });
1498 m_delayedSlotTextChangedTimer->callOnTimeout(m_lineEdit, [this]() {
1499 _k_slotTextChanged(m_lineEdit->text());
1500 });
1501
1502 QObject::connect(m_fileDialog, &QDialog::accepted, q, [this]() {
1503 slotCreateDirectory();
1504 });
1505 QObject::connect(m_fileDialog, &QDialog::rejected, q, [this]() {
1506 slotAbortDialog();
1507 });
1508
1509 m_fileDialog->show();
1510 m_lineEdit->selectAll();
1511 m_lineEdit->setFocus();
1512}
1513
1515{
1516 if (d->m_popupFiles.isEmpty()) {
1518 return;
1519 }
1520
1521 checkUpToDate();
1522 if (!d->m_firstFileEntry) {
1524 return;
1525 }
1526
1527 if (!d->m_isCreateFileRunning) {
1528 d->executeRealFileOrDir(*d->m_firstFileEntry);
1529 } else {
1530 qCWarning(KFILEWIDGETS_LOG) << "File creation is already running for " << d->m_firstFileEntry;
1531 }
1532}
1533
1535{
1536 return d->m_isCreateFileRunning;
1537}
1538
1540{
1541 return d->m_modal;
1542}
1543
1545{
1546 d->m_modal = modal;
1547}
1548
1550{
1551 d->m_parentWidget = parentWidget;
1552}
1553
1555{
1556 d->m_supportedMimeTypes = mime;
1557}
1558
1560{
1561 if (job->error()) {
1562 if (job->error() == KIO::ERR_DIR_ALREADY_EXIST && d->m_selectDirWhenAlreadyExists) {
1563 auto *simpleJob = ::qobject_cast<KIO::SimpleJob *>(job);
1564 if (simpleJob) {
1565 const QUrl jobUrl = simpleJob->url();
1566 // Select the existing dir
1567 Q_EMIT selectExistingDir(jobUrl);
1568 }
1569 } else { // All other errors
1570 static_cast<KIO::Job *>(job)->uiDelegate()->showErrorMessage();
1571 }
1572 } else {
1573 // Was this a copy or a mkdir?
1574 if (job->property("newDirectoryURL").isValid()) {
1575 QUrl newDirectoryURL = job->property("newDirectoryURL").toUrl();
1576 Q_EMIT directoryCreated(newDirectoryURL);
1577 } else {
1579 if (copyJob) {
1580 const QUrl destUrl = copyJob->destUrl();
1581 const QUrl localUrl = d->mostLocalUrl(destUrl);
1582 if (localUrl.isLocalFile()) {
1583 // Normal (local) file. Need to "touch" it, kio_file copied the mtime.
1584 (void)::utime(QFile::encodeName(localUrl.toLocalFile()).constData(), nullptr);
1585 }
1586 Q_EMIT fileCreated(destUrl);
1587 } else if (KIO::SimpleJob *simpleJob = ::qobject_cast<KIO::SimpleJob *>(job)) {
1588 // Called in the storedPut() case
1589#ifdef WITH_QTDBUS
1590 org::kde::KDirNotify::emitFilesAdded(simpleJob->url().adjusted(QUrl::RemoveFilename | QUrl::StripTrailingSlash));
1591#endif
1592 Q_EMIT fileCreated(simpleJob->url());
1593 }
1594 }
1595 }
1596 if (!d->m_tempFileToDelete.isEmpty()) {
1597 QFile::remove(d->m_tempFileToDelete);
1598 }
1599}
1600
1602{
1603 return d->m_supportedMimeTypes;
1604}
1605
1607{
1608 d->m_popupFiles = {directory};
1609
1610 if (directory.isEmpty()) {
1611 d->m_newMenuGroup->setEnabled(false);
1612 } else {
1613 if (KProtocolManager::supportsWriting(directory)) {
1614 d->m_newMenuGroup->setEnabled(true);
1615 if (d->m_newDirAction) {
1616 d->m_newDirAction->setEnabled(KProtocolManager::supportsMakeDir(directory)); // e.g. trash:/
1617 }
1618 } else {
1619 d->m_newMenuGroup->setEnabled(true);
1620 }
1621 }
1622}
1623
1625{
1626 return d->m_popupFiles.isEmpty() ? QUrl() : d->m_popupFiles.first();
1627}
1628
1630{
1631 d->m_newFolderShortcutAction = action;
1632}
1633
1635{
1636 d->m_newFileShortcutAction = action;
1637}
1638
1639#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:2577
QUrl destUrl() const
Returns the destination URL.
Definition copyjob.cpp:449
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:2672
@ 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:2612
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
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)
QString locate(StandardLocation type, const QString &fileName, LocateOptions options)
QStringList locateAll(StandardLocation type, const QString &fileName, LocateOptions options)
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 Fri Oct 11 2024 12:11:14 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.