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

KDE's Doxygen guidelines are available online.