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

KDE's Doxygen guidelines are available online.