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