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

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