1/* This file is part of the KDE project
2 Copyright (C) 1998-2009 David Faure <faure@kde.org>
3 2003 Sven Leiber <s.leiber@web.de>
4
5 This library is free software; you can redistribute it and/or
6 modify it under the terms of the GNU Library General Public
7 License as published by the Free Software Foundation; either
8 version 2 or at your option version 3.
9
10 This library is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY; without even the implied warranty of
12 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 Library General Public License for more details.
14
15 You should have received a copy of the GNU Library General Public License
16 along with this library; see the file COPYING.LIB. If not, write to
17 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 Boston, MA 02110-1301, USA.
19*/
20
21#include "knewfilemenu.h"
22#include "knameandurlinputdialog.h"
23
24#include <QDir>
25#include <QVBoxLayout>
26#include <QList>
27#include <QLabel>
28#include <kactioncollection.h>
29#include <kdebug.h>
30#include <kdesktopfile.h>
31#include <kdirwatch.h>
32#include <kicon.h>
33#include <kcomponentdata.h>
34#include <kinputdialog.h>
35#include <kdialog.h>
36#include <klocale.h>
37#include <klineedit.h>
38#include <kmessagebox.h>
39#include <kstandarddirs.h>
40#include <kprotocolinfo.h>
41#include <kprotocolmanager.h>
42#include <kmenu.h>
43#include <krun.h>
44#include <kshell.h>
45#include <kio/job.h>
46#include <kio/copyjob.h>
47#include <kio/jobuidelegate.h>
48#include <kio/renamedialog.h>
49#include <kio/netaccess.h>
50#include <kio/fileundomanager.h>
51#include <kio/kurifilter.h>
52
53#include <kpropertiesdialog.h>
54#include <ktemporaryfile.h>
55#include <utime.h>
56
57static QString expandTilde(const QString& name, bool isfile = false)
58{
59 if (!name.isEmpty() && (!isfile || name[0] == '\\'))
60 {
61 const QString expandedName = KShell::tildeExpand(name);
62 // When a tilde mark cannot be properly expanded, the above call
63 // returns an empty string...
64 if (!expandedName.isEmpty())
65 return expandedName;
66 }
67
68 return name;
69}
70
71// Singleton, with data shared by all KNewFileMenu instances
72class KNewFileMenuSingleton
73{
74public:
75 KNewFileMenuSingleton()
76 : dirWatch(0),
77 filesParsed(false),
78 templatesList(0),
79 templatesVersion(0)
80 {
81 }
82
83 ~KNewFileMenuSingleton()
84 {
85 delete dirWatch;
86 delete templatesList;
87 }
88
89
90 /**
91 * Opens the desktop files and completes the Entry list
92 * Input: the entry list. Output: the entry list ;-)
93 */
94 void parseFiles();
95
96 /**
97 * For entryType
98 * LINKTOTEMPLATE: a desktop file that points to a file or dir to copy
99 * TEMPLATE: a real file to copy as is (the KDE-1.x solution)
100 * SEPARATOR: to put a separator in the menu
101 * 0 means: not parsed, i.e. we don't know
102 */
103 enum EntryType { Unknown, LinkToTemplate = 1, Template, Separator };
104
105 KDirWatch * dirWatch;
106
107 struct Entry {
108 QString text;
109 QString filePath; // empty for Separator
110 QString templatePath; // same as filePath for Template
111 QString icon;
112 EntryType entryType;
113 QString comment;
114 QString mimeType;
115 };
116 // NOTE: only filePath is known before we call parseFiles
117
118 /**
119 * List of all template files. It is important that they are in
120 * the same order as the 'New' menu.
121 */
122 typedef QList<Entry> EntryList;
123
124 /**
125 * Set back to false each time new templates are found,
126 * and to true on the first call to parseFiles
127 */
128 bool filesParsed;
129 EntryList * templatesList;
130
131 /**
132 * Is increased when templatesList has been updated and
133 * menu needs to be re-filled. Menus have their own version and compare it
134 * to templatesVersion before showing up
135 */
136 int templatesVersion;
137};
138
139void KNewFileMenuSingleton::parseFiles()
140{
141 //kDebug(1203);
142 filesParsed = true;
143 QMutableListIterator<KNewFileMenuSingleton::Entry> templIter(*templatesList);
144 while (templIter.hasNext()) {
145 KNewFileMenuSingleton::Entry& templ = templIter.next();
146 const QString filePath = templ.filePath;
147 if (!filePath.isEmpty())
148 {
149 QString text;
150 QString templatePath;
151 // If a desktop file, then read the name from it.
152 // Otherwise (or if no name in it?) use file name
153 if (KDesktopFile::isDesktopFile(filePath)) {
154 KDesktopFile desktopFile( filePath);
155 if (desktopFile.noDisplay()) {
156 templIter.remove();
157 continue;
158 }
159 text = desktopFile.readName();
160 templ.icon = desktopFile.readIcon();
161 templ.comment = desktopFile.readComment();
162 QString type = desktopFile.readType();
163 if (type == "Link")
164 {
165 templatePath = desktopFile.desktopGroup().readPathEntry("URL", QString());
166 if (templatePath[0] != '/' && !templatePath.startsWith("__"))
167 {
168 if (templatePath.startsWith("file:/"))
169 templatePath = KUrl(templatePath).toLocalFile();
170 else
171 {
172 // A relative path, then (that's the default in the files we ship)
173 QString linkDir = filePath.left(filePath.lastIndexOf('/') + 1 /*keep / */);
174 //kDebug(1203) << "linkDir=" << linkDir;
175 templatePath = linkDir + templatePath;
176 }
177 }
178 }
179 if (templatePath.isEmpty())
180 {
181 // No URL key, this is an old-style template
182 templ.entryType = KNewFileMenuSingleton::Template;
183 templ.templatePath = templ.filePath; // we'll copy the file
184 } else {
185 templ.entryType = KNewFileMenuSingleton::LinkToTemplate;
186 templ.templatePath = templatePath;
187 }
188
189 }
190 if (text.isEmpty())
191 {
192 text = KUrl(filePath).fileName();
193 if (text.endsWith(".desktop"))
194 text.truncate(text.length() - 8);
195 }
196 templ.text = text;
197 /*kDebug(1203) << "Updating entry with text=" << text
198 << "entryType=" << templ.entryType
199 << "templatePath=" << templ.templatePath;*/
200 }
201 else {
202 templ.entryType = KNewFileMenuSingleton::Separator;
203 }
204 }
205}
206
207K_GLOBAL_STATIC(KNewFileMenuSingleton, kNewMenuGlobals)
208
209class KNewFileMenuCopyData
210{
211public:
212 KNewFileMenuCopyData() { m_isSymlink = false;}
213 ~KNewFileMenuCopyData() {}
214 QString chosenFileName() const { return m_chosenFileName; }
215
216 // If empty, no copy is performed.
217 QString sourceFileToCopy() const { return m_src; }
218 QString tempFileToDelete() const { return m_tempFileToDelete; }
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 KNewFileMenuPrivate(KNewFileMenu* qq)
231 : m_menuItemsVersion(0),
232 m_modal(true),
233 m_viewShowsHiddenFiles(false),
234 q(qq)
235 {}
236
237 bool checkSourceExists(const QString& src);
238
239 /**
240 * Asks user whether to create a hidden directory with a dialog
241 */
242 void confirmCreatingHiddenDir(const QString& name);
243
244 /**
245 * The strategy used for other desktop files than Type=Link. Example: Application, Device.
246 */
247 void executeOtherDesktopFile(const KNewFileMenuSingleton::Entry& entry);
248
249 /**
250 * The strategy used for "real files or directories" (the common case)
251 */
252 void executeRealFileOrDir(const KNewFileMenuSingleton::Entry& entry);
253
254 /**
255 * Actually performs file handling. Reads in m_copyData for needed data, that has been collected by execute*() before
256 */
257 void executeStrategy();
258
259 /**
260 * The strategy used when creating a symlink
261 */
262 void executeSymLink(const KNewFileMenuSingleton::Entry& entry);
263
264 /**
265 * The strategy used for "url" desktop files
266 */
267 void executeUrlDesktopFile(const KNewFileMenuSingleton::Entry& entry);
268
269 /**
270 * Fills the menu from the templates list.
271 */
272 void fillMenu();
273
274 /**
275 * Just clears the string buffer d->m_text, but I need a slot for this to occur
276 */
277 void _k_slotAbortDialog();
278
279 /**
280 * Called when New->* is clicked
281 */
282 void _k_slotActionTriggered(QAction* action);
283
284 /**
285 * Callback function that reads in directory name from dialog and processes it
286 */
287 void _k_slotCreateDirectory(bool writeHiddenDir = false);
288
289 /**
290 * Callback function that reads in directory name from dialog and processes it. This will wirte
291 * a hidden directory without further questions
292 */
293 void _k_slotCreateHiddenDirectory();
294
295 /**
296 * Fills the templates list.
297 */
298 void _k_slotFillTemplates();
299
300 /**
301 * Callback in KNewFileMenu for the OtherDesktopFile Dialog. Handles dialog input and gives over
302 * to executeStrategy()
303 */
304 void _k_slotOtherDesktopFile();
305
306 /**
307 * Callback in KNewFileMenu for the RealFile Dialog. Handles dialog input and gives over
308 * to executeStrategy()
309 */
310 void _k_slotRealFileOrDir();
311
312 /**
313 * Dialogs use this slot to write the changed string into KNewFile menu when the user
314 * changes touches them
315 */
316 void _k_slotTextChanged(const QString & text);
317
318 /**
319 * Callback in KNewFileMenu for the Symlink Dialog. Handles dialog input and gives over
320 * to executeStrategy()
321 */
322 void _k_slotSymLink();
323
324 /**
325 * Callback in KNewFileMenu for the Url/Desktop Dialog. Handles dialog input and gives over
326 * to executeStrategy()
327 */
328 void _k_slotUrlDesktopFile();
329
330
331 KActionCollection * m_actionCollection;
332 KDialog* m_fileDialog;
333
334 KActionMenu *m_menuDev;
335 int m_menuItemsVersion;
336 bool m_modal;
337 QAction* m_newDirAction;
338
339 /**
340 * The action group that our actions belong to
341 */
342 QActionGroup* m_newMenuGroup;
343 QWidget *m_parentWidget;
344
345 /**
346 * When the user pressed the right mouse button over an URL a popup menu
347 * is displayed. The URL belonging to this popup menu is stored here.
348 */
349 KUrl::List m_popupFiles;
350
351 QStringList m_supportedMimeTypes;
352 QString m_tempFileToDelete; // set when a tempfile was created for a Type=URL desktop file
353 QString m_text;
354 bool m_viewShowsHiddenFiles;
355
356 KNewFileMenu* q;
357
358 KNewFileMenuCopyData m_copyData;
359};
360
361bool KNewFileMenuPrivate::checkSourceExists(const QString& src)
362{
363 if (!QFile::exists(src)) {
364 kWarning(1203) << src << "doesn't exist" ;
365
366 KDialog* dialog = new KDialog(m_parentWidget);
367 dialog->setCaption( i18n("Sorry") );
368 dialog->setButtons( KDialog::Ok );
369 dialog->setObjectName( "sorry" );
370 dialog->setModal(q->isModal());
371 dialog->setAttribute(Qt::WA_DeleteOnClose);
372 dialog->setDefaultButton( KDialog::Ok );
373 dialog->setEscapeButton( KDialog::Ok );
374
375 KMessageBox::createKMessageBox(dialog, QMessageBox::Warning,
376 i18n("<qt>The template file <b>%1</b> does not exist.</qt>", src),
377 QStringList(), QString(), 0, KMessageBox::NoExec,
378 QString());
379
380 dialog->show();
381
382 return false;
383 }
384 return true;
385}
386
387void KNewFileMenuPrivate::confirmCreatingHiddenDir(const QString& name)
388{
389 if(!KMessageBox::shouldBeShownContinue("confirm_create_hidden_dir")){
390 _k_slotCreateHiddenDirectory();
391 return;
392 }
393
394 KGuiItem continueGuiItem(KStandardGuiItem::cont());
395 continueGuiItem.setText(i18nc("@action:button", "Create directory"));
396 KGuiItem cancelGuiItem(KStandardGuiItem::cancel());
397 cancelGuiItem.setText(i18nc("@action:button", "Enter a different name"));
398
399 KDialog* confirmDialog = new KDialog(m_parentWidget);
400 confirmDialog->setCaption(i18n("Create hidden directory?"));
401 confirmDialog->setModal(m_modal);
402 confirmDialog->setAttribute(Qt::WA_DeleteOnClose);
403 KMessageBox::createKMessageBox(confirmDialog, QMessageBox::Warning,
404 i18n("The name \"%1\" starts with a dot, so the directory will be hidden by default.", name),
405 QStringList(),
406 i18n("Do not ask again"),
407 0,
408 KMessageBox::NoExec,
409 QString());
410 confirmDialog->setButtonGuiItem(KDialog::Ok, continueGuiItem);
411 confirmDialog->setButtonGuiItem(KDialog::Cancel, cancelGuiItem);
412
413 QObject::connect(confirmDialog, SIGNAL(accepted()), q, SLOT(_k_slotCreateHiddenDirectory()));
414 QObject::connect(confirmDialog, SIGNAL(rejected()), q, SLOT(createDirectory()));
415
416 m_fileDialog = confirmDialog;
417 confirmDialog->show();
418
419}
420
421void KNewFileMenuPrivate::executeOtherDesktopFile(const KNewFileMenuSingleton::Entry& entry)
422{
423 if (!checkSourceExists(entry.templatePath)) {
424 return;
425 }
426
427 KUrl::List::const_iterator it = m_popupFiles.constBegin();
428 for (; it != m_popupFiles.constEnd(); ++it)
429 {
430 QString text = entry.text;
431 text.remove("..."); // the ... is fine for the menu item but not for the default filename
432 text = text.trimmed(); // In some languages, there is a space in front of "...", see bug 268895
433 // KDE5 TODO: remove the "..." from link*.desktop files and use i18n("%1...") when making
434 // the action.
435
436 KUrl defaultFile(*it);
437 defaultFile.addPath(KIO::encodeFileName(text));
438 if (defaultFile.isLocalFile() && QFile::exists(defaultFile.toLocalFile()))
439 text = KIO::RenameDialog::suggestName(*it, text);
440
441 const KUrl templateUrl(entry.templatePath);
442
443 KDialog* dlg = new KPropertiesDialog(templateUrl, *it, text, m_parentWidget);
444 dlg->setModal(q->isModal());
445 dlg->setAttribute(Qt::WA_DeleteOnClose);
446 QObject::connect(dlg, SIGNAL(applied()), q, SLOT(_k_slotOtherDesktopFile()));
447 dlg->show();
448 }
449 // We don't set m_src here -> there will be no copy, we are done.
450}
451
452void KNewFileMenuPrivate::executeRealFileOrDir(const KNewFileMenuSingleton::Entry& entry)
453{
454 // The template is not a desktop file
455 // Show the small dialog for getting the destination filename
456 QString text = entry.text;
457 text.remove("..."); // the ... is fine for the menu item but not for the default filename
458 text = text.trimmed(); // In some languages, there is a space in front of "...", see bug 268895
459 m_copyData.m_src = entry.templatePath;
460
461 KUrl defaultFile(m_popupFiles.first());
462 defaultFile.addPath(KIO::encodeFileName(text));
463 if (defaultFile.isLocalFile() && QFile::exists(defaultFile.toLocalFile()))
464 text = KIO::RenameDialog::suggestName(m_popupFiles.first(), text);
465
466 KDialog* fileDialog = new KDialog(m_parentWidget);
467 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
468 fileDialog->setModal(q->isModal());
469 fileDialog->setButtons(KDialog::Ok | KDialog::Cancel);
470
471 QWidget* mainWidget = new QWidget(fileDialog);
472 QVBoxLayout *layout = new QVBoxLayout(mainWidget);
473 QLabel *label = new QLabel(entry.comment);
474
475 // We don't set the text of lineEdit in its constructor because the clear button would not be shown then.
476 // It seems that setClearButtonShown(true) must be called *before* the text is set to make it work.
477 // TODO: should probably be investigated and fixed in KLineEdit.
478 KLineEdit *lineEdit = new KLineEdit;
479 lineEdit->setClearButtonShown(true);
480 lineEdit->setText(text);
481
482 _k_slotTextChanged(text);
483 QObject::connect(lineEdit, SIGNAL(textChanged(QString)), q, SLOT(_k_slotTextChanged(QString)));
484
485 layout->addWidget(label);
486 layout->addWidget(lineEdit);
487
488 fileDialog->setMainWidget(mainWidget);
489 QObject::connect(fileDialog, SIGNAL(accepted()), q, SLOT(_k_slotRealFileOrDir()));
490 QObject::connect(fileDialog, SIGNAL(rejected()), q, SLOT(_k_slotAbortDialog()));
491
492 fileDialog->show();
493 lineEdit->selectAll();
494 lineEdit->setFocus();
495}
496
497void KNewFileMenuPrivate::executeSymLink(const KNewFileMenuSingleton::Entry& entry)
498{
499 KNameAndUrlInputDialog* dlg = new KNameAndUrlInputDialog(i18n("File name:"), entry.comment, m_popupFiles.first(), m_parentWidget);
500 dlg->setModal(q->isModal());
501 dlg->setAttribute(Qt::WA_DeleteOnClose);
502 dlg->setCaption(i18n("Create Symlink"));
503 m_fileDialog = dlg;
504 QObject::connect(dlg, SIGNAL(accepted()), q, SLOT(_k_slotSymLink()));
505 dlg->show();
506}
507
508void KNewFileMenuPrivate::executeStrategy()
509{
510 m_tempFileToDelete = m_copyData.tempFileToDelete();
511 const QString src = m_copyData.sourceFileToCopy();
512 QString chosenFileName = expandTilde(m_copyData.chosenFileName(), true);
513
514 if (src.isEmpty())
515 return;
516 KUrl uSrc(src);
517 if (uSrc.isLocalFile()) {
518 // In case the templates/.source directory contains symlinks, resolve
519 // them to the target files. Fixes bug #149628.
520 KFileItem item(uSrc, QString(), KFileItem::Unknown);
521 if (item.isLink())
522 uSrc.setPath(item.linkDest());
523
524 if (!m_copyData.m_isSymlink) {
525 // If the file is not going to be detected as a desktop file, due to a
526 // known extension (e.g. ".pl"), append ".desktop". #224142.
527 QFile srcFile(uSrc.toLocalFile());
528 if (srcFile.open(QIODevice::ReadOnly)) {
529 KMimeType::Ptr wantedMime = KMimeType::findByUrl(uSrc);
530 KMimeType::Ptr mime = KMimeType::findByNameAndContent(m_copyData.m_chosenFileName, srcFile.read(1024));
531 //kDebug() << "mime=" << mime->name() << "wantedMime=" << wantedMime->name();
532 if (!mime->is(wantedMime->name()))
533 chosenFileName += wantedMime->mainExtension();
534 }
535 }
536 }
537
538 // The template is not a desktop file [or it's a URL one]
539 // Copy it.
540 KUrl::List::const_iterator it = m_popupFiles.constBegin();
541 for (; it != m_popupFiles.constEnd(); ++it)
542 {
543 KUrl dest(*it);
544 dest.addPath(KIO::encodeFileName(chosenFileName));
545
546 KUrl::List lstSrc;
547 lstSrc.append(uSrc);
548 KIO::Job* kjob;
549 if (m_copyData.m_isSymlink) {
550 kjob = KIO::symlink(src, dest);
551 // This doesn't work, FileUndoManager registers new links in copyingLinkDone,
552 // which KIO::symlink obviously doesn't emit... Needs code in FileUndoManager.
553 //KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Link, lstSrc, dest, kjob);
554 } else {
555 //kDebug(1203) << "KIO::copyAs(" << uSrc.url() << "," << dest.url() << ")";
556 KIO::CopyJob * job = KIO::copyAs(uSrc, dest);
557 job->setDefaultPermissions(true);
558 kjob = job;
559 KIO::FileUndoManager::self()->recordJob(KIO::FileUndoManager::Copy, lstSrc, dest, job);
560 }
561 kjob->ui()->setWindow(m_parentWidget);
562 QObject::connect(kjob, SIGNAL(result(KJob*)), q, SLOT(slotResult(KJob*)));
563 }
564}
565
566void KNewFileMenuPrivate::executeUrlDesktopFile(const KNewFileMenuSingleton::Entry& entry)
567{
568 KNameAndUrlInputDialog* dlg = new KNameAndUrlInputDialog(i18n("File name:"), entry.comment, m_popupFiles.first(), m_parentWidget);
569 m_copyData.m_templatePath = entry.templatePath;
570 dlg->setModal(q->isModal());
571 dlg->setAttribute(Qt::WA_DeleteOnClose);
572 dlg->setCaption(i18n("Create link to URL"));
573 m_fileDialog = dlg;
574 QObject::connect(dlg, SIGNAL(accepted()), q, SLOT(_k_slotUrlDesktopFile()));
575 dlg->show();
576}
577
578void KNewFileMenuPrivate::fillMenu()
579{
580 QMenu* menu = q->menu();
581 menu->clear();
582 m_menuDev->menu()->clear();
583 m_newDirAction = 0;
584
585 QSet<QString> seenTexts;
586 // these shall be put at special positions
587 QAction* linkURL = 0;
588 QAction* linkApp = 0;
589 QAction* linkPath = 0;
590
591 KNewFileMenuSingleton* s = kNewMenuGlobals;
592 int i = 1;
593 KNewFileMenuSingleton::EntryList::iterator templ = s->templatesList->begin();
594 const KNewFileMenuSingleton::EntryList::iterator templ_end = s->templatesList->end();
595 for (; templ != templ_end; ++templ, ++i)
596 {
597 KNewFileMenuSingleton::Entry& entry = *templ;
598 if (entry.entryType != KNewFileMenuSingleton::Separator) {
599 // There might be a .desktop for that one already, if it's a kdelnk
600 // This assumes we read .desktop files before .kdelnk files ...
601
602 // In fact, we skip any second item that has the same text as another one.
603 // Duplicates in a menu look bad in any case.
604
605 const bool bSkip = seenTexts.contains(entry.text);
606 if (bSkip) {
607 kDebug(1203) << "skipping" << entry.filePath;
608 } else {
609 seenTexts.insert(entry.text);
610 //const KNewFileMenuSingleton::Entry entry = templatesList->at(i-1);
611
612 const QString templatePath = entry.templatePath;
613 // The best way to identify the "Create Directory", "Link to Location", "Link to Application" was the template
614 if (templatePath.endsWith("emptydir")) {
615 QAction * act = new QAction(q);
616 m_newDirAction = act;
617 act->setIcon(KIcon(entry.icon));
618 act->setText(i18nc("@item:inmenu Create New", "%1", entry.text));
619 act->setActionGroup(m_newMenuGroup);
620 menu->addAction(act);
621
622 QAction *sep = new QAction(q);
623 sep->setSeparator(true);
624 menu->addAction(sep);
625 } else {
626
627 if (!m_supportedMimeTypes.isEmpty()) {
628 bool keep = false;
629
630 // We need to do mimetype filtering, for real files.
631 const bool createSymlink = entry.templatePath == "__CREATE_SYMLINK__";
632 if (createSymlink) {
633 keep = true;
634 } else if (!KDesktopFile::isDesktopFile(entry.templatePath)) {
635
636 // Determine mimetype on demand
637 KMimeType::Ptr mime;
638 if (entry.mimeType.isEmpty()) {
639 mime = KMimeType::findByPath(entry.templatePath);
640 if (mime) {
641 //kDebug() << entry.templatePath << "is" << mime->name();
642 entry.mimeType = mime->name();
643 } else {
644 entry.mimeType = KMimeType::defaultMimeType();
645 }
646 } else {
647 mime = KMimeType::mimeType(entry.mimeType);
648 }
649 Q_FOREACH(const QString& supportedMime, m_supportedMimeTypes) {
650 if (mime && mime->is(supportedMime)) {
651 keep = true;
652 break;
653 }
654 }
655 }
656
657 if (!keep) {
658 //kDebug() << "Not keeping" << entry.templatePath;
659 continue;
660 }
661 }
662
663 QAction * act = new QAction(q);
664 act->setData(i);
665 act->setIcon(KIcon(entry.icon));
666 act->setText(i18nc("@item:inmenu Create New", "%1", entry.text));
667 act->setActionGroup(m_newMenuGroup);
668
669 //kDebug() << templatePath << entry.filePath;
670
671 if (templatePath.endsWith("/URL.desktop")) {
672 linkURL = act;
673 } else if (templatePath.endsWith("/Program.desktop")) {
674 linkApp = act;
675 } else if (entry.filePath.endsWith("/linkPath.desktop")) {
676 linkPath = act;
677 } else if (KDesktopFile::isDesktopFile(templatePath)) {
678 KDesktopFile df(templatePath);
679 if (df.readType() == "FSDevice")
680 m_menuDev->menu()->addAction(act);
681 else
682 menu->addAction(act);
683 }
684 else
685 {
686 menu->addAction(act);
687 }
688 }
689 }
690 } else { // Separate system from personal templates
691 Q_ASSERT(entry.entryType != 0);
692
693 QAction *sep = new QAction(q);
694 sep->setSeparator(true);
695 menu->addAction(sep);
696 }
697 }
698
699 if (m_supportedMimeTypes.isEmpty()) {
700 QAction *sep = new QAction(q);
701 sep->setSeparator(true);
702 menu->addAction(sep);
703 if (linkURL) menu->addAction(linkURL);
704 if (linkPath) menu->addAction(linkPath);
705 if (linkApp) menu->addAction(linkApp);
706 Q_ASSERT(m_menuDev);
707 menu->addAction(m_menuDev);
708 }
709}
710
711void KNewFileMenuPrivate::_k_slotAbortDialog()
712{
713 m_text = QString();
714}
715
716void KNewFileMenuPrivate::_k_slotActionTriggered(QAction* action)
717{
718 q->trigger(); // was for kdesktop's slotNewMenuActivated() in kde3 times. Can't hurt to keep it...
719
720 if (action == m_newDirAction) {
721 q->createDirectory();
722 return;
723 }
724 const int id = action->data().toInt();
725 Q_ASSERT(id > 0);
726
727 KNewFileMenuSingleton* s = kNewMenuGlobals;
728 const KNewFileMenuSingleton::Entry entry = s->templatesList->at(id - 1);
729
730 const bool createSymlink = entry.templatePath == "__CREATE_SYMLINK__";
731
732 m_copyData = KNewFileMenuCopyData();
733
734 if (createSymlink) {
735 m_copyData.m_isSymlink = true;
736 executeSymLink(entry);
737 }
738 else if (KDesktopFile::isDesktopFile(entry.templatePath)) {
739 KDesktopFile df(entry.templatePath);
740 if (df.readType() == "Link") {
741 executeUrlDesktopFile(entry);
742 } else { // any other desktop file (Device, App, etc.)
743 executeOtherDesktopFile(entry);
744 }
745 }
746 else {
747 executeRealFileOrDir(entry);
748 }
749
750}
751
752void KNewFileMenuPrivate::_k_slotCreateDirectory(bool writeHiddenDir)
753{
754 KUrl url;
755 KUrl baseUrl = m_popupFiles.first();
756 bool askAgain = false;
757
758 QString name = expandTilde(m_text);
759
760 if (!name.isEmpty()) {
761 if ((name[0] == '/'))
762 url.setPath(name);
763 else {
764 if (!m_viewShowsHiddenFiles && name.startsWith('.')) {
765 if (!writeHiddenDir) {
766 confirmCreatingHiddenDir(name);
767 return;
768 }
769 }
770 name = KIO::encodeFileName( name );
771 url = baseUrl;
772 url.addPath( name );
773 }
774 }
775
776 if (!askAgain) {
777 KIO::SimpleJob * job = KIO::mkdir(url);
778 job->setProperty("isMkdirJob", true); // KDE5: cast to MkdirJob in slotResult instead
779 job->ui()->setWindow(m_parentWidget);
780 job->ui()->setAutoErrorHandlingEnabled(true);
781 KIO::FileUndoManager::self()->recordJob( KIO::FileUndoManager::Mkdir, KUrl(), url, job );
782
783 if (job) {
784 // We want the error handling to be done by slotResult so that subclasses can reimplement it
785 job->ui()->setAutoErrorHandlingEnabled(false);
786 QObject::connect(job, SIGNAL(result(KJob*)), q, SLOT(slotResult(KJob*)));
787 }
788 }
789 else {
790 q->createDirectory(); // ask again for the name
791 }
792 _k_slotAbortDialog();
793}
794
795void KNewFileMenuPrivate::_k_slotCreateHiddenDirectory()
796{
797 _k_slotCreateDirectory(true);
798}
799
800void KNewFileMenuPrivate::_k_slotFillTemplates()
801{
802 KNewFileMenuSingleton* s = kNewMenuGlobals;
803 //kDebug(1203);
804 // Ensure any changes in the templates dir will call this
805 if (! s->dirWatch) {
806 s->dirWatch = new KDirWatch;
807 const QStringList dirs = m_actionCollection->componentData().dirs()->resourceDirs("templates");
808 for (QStringList::const_iterator it = dirs.constBegin() ; it != dirs.constEnd() ; ++it) {
809 //kDebug(1203) << "Templates resource dir:" << *it;
810 s->dirWatch->addDir(*it);
811 }
812 QObject::connect(s->dirWatch, SIGNAL(dirty(QString)),
813 q, SLOT(_k_slotFillTemplates()));
814 QObject::connect(s->dirWatch, SIGNAL(created(QString)),
815 q, SLOT(_k_slotFillTemplates()));
816 QObject::connect(s->dirWatch, SIGNAL(deleted(QString)),
817 q, SLOT(_k_slotFillTemplates()));
818 // Ok, this doesn't cope with new dirs in KDEDIRS, but that's another story
819 }
820 ++s->templatesVersion;
821 s->filesParsed = false;
822
823 s->templatesList->clear();
824
825 // Look into "templates" dirs.
826 const QStringList files = m_actionCollection->componentData().dirs()->findAllResources("templates");
827 QMap<QString, KNewFileMenuSingleton::Entry> slist; // used for sorting
828 Q_FOREACH(const QString& file, files) {
829 //kDebug(1203) << file;
830 if (file[0] != '.') {
831 KNewFileMenuSingleton::Entry e;
832 e.filePath = file;
833 e.entryType = KNewFileMenuSingleton::Unknown; // not parsed yet
834
835 // Put Directory first in the list (a bit hacky),
836 // and TextFile before others because it's the most used one.
837 // This also sorts by user-visible name.
838 // The rest of the re-ordering is done in fillMenu.
839 const KDesktopFile config(file);
840 QString key = config.desktopGroup().readEntry("Name");
841 if (file.endsWith("Directory.desktop")) {
842 key.prepend('0');
843 } else if (file.endsWith("TextFile.desktop")) {
844 key.prepend('1');
845 } else {
846 key.prepend('2');
847 }
848 slist.insert(key, e);
849 }
850 }
851 (*s->templatesList) += slist.values();
852}
853
854void KNewFileMenuPrivate::_k_slotOtherDesktopFile()
855{
856 executeStrategy();
857}
858
859void KNewFileMenuPrivate::_k_slotRealFileOrDir()
860{
861 m_copyData.m_chosenFileName = m_text;
862 _k_slotAbortDialog();
863 executeStrategy();
864}
865
866void KNewFileMenuPrivate::_k_slotSymLink()
867{
868 KNameAndUrlInputDialog* dlg = static_cast<KNameAndUrlInputDialog*>(m_fileDialog);
869
870 m_copyData.m_chosenFileName = dlg->name(); // no path
871 KUrl linkUrl = dlg->url(); // the url to put in the file
872
873 if (m_copyData.m_chosenFileName.isEmpty() || linkUrl.isEmpty())
874 return;
875
876 if (linkUrl.isRelative())
877 m_copyData.m_src = QUrl(linkUrl).toString();
878 else if (linkUrl.isLocalFile())
879 m_copyData.m_src = linkUrl.toLocalFile();
880 else {
881 KDialog* dialog = new KDialog(m_parentWidget);
882 dialog->setCaption( i18n("Sorry") );
883 dialog->setButtons( KDialog::Ok );
884 dialog->setObjectName( "sorry" );
885 dialog->setModal(m_modal);
886 dialog->setAttribute(Qt::WA_DeleteOnClose);
887 dialog->setDefaultButton( KDialog::Ok );
888 dialog->setEscapeButton( KDialog::Ok );
889 m_fileDialog = dialog;
890
891 KMessageBox::createKMessageBox(dialog, QMessageBox::Warning,
892 i18n("Basic links can only point to local files or directories.\nPlease use \"Link to Location\" for remote URLs."),
893 QStringList(), QString(), 0, KMessageBox::NoExec,
894 QString());
895
896 dialog->show();
897 return;
898 }
899 executeStrategy();
900}
901
902void KNewFileMenuPrivate::_k_slotTextChanged(const QString & text)
903{
904 m_text = text;
905}
906
907void KNewFileMenuPrivate::_k_slotUrlDesktopFile()
908{
909 KNameAndUrlInputDialog* dlg = static_cast<KNameAndUrlInputDialog*>(m_fileDialog);
910
911 m_copyData.m_chosenFileName = dlg->name(); // no path
912 KUrl linkUrl = dlg->url();
913
914 // Filter user input so that short uri entries, e.g. www.kde.org, are
915 // handled properly. This not only makes the icon detection below work
916 // properly, but opening the URL link where the short uri will not be
917 // sent to the application (opening such link Konqueror fails).
918 KUriFilterData uriData;
919 uriData.setData(linkUrl); // the url to put in the file
920 uriData.setCheckForExecutables(false);
921
922 if (KUriFilter::self()->filterUri(uriData, QStringList() << QLatin1String("kshorturifilter"))) {
923 linkUrl = uriData.uri();
924 }
925
926 if (m_copyData.m_chosenFileName.isEmpty() || linkUrl.isEmpty())
927 return;
928
929 // It's a "URL" desktop file; we need to make a temp copy of it, to modify it
930 // before copying it to the final destination [which could be a remote protocol]
931 KTemporaryFile tmpFile;
932 tmpFile.setAutoRemove(false); // done below
933 if (!tmpFile.open()) {
934 kError() << "Couldn't create temp file!";
935 return;
936 }
937
938 if (!checkSourceExists(m_copyData.m_templatePath)) {
939 return;
940 }
941
942 // First copy the template into the temp file
943 QFile file(m_copyData.m_templatePath);
944 if (!file.open(QIODevice::ReadOnly)) {
945 kError() << "Couldn't open template" << m_copyData.m_templatePath;
946 return;
947 }
948 const QByteArray data = file.readAll();
949 tmpFile.write(data);
950 const QString tempFileName = tmpFile.fileName();
951 Q_ASSERT(!tempFileName.isEmpty());
952 tmpFile.close();
953 file.close();
954
955 KDesktopFile df(tempFileName);
956 KConfigGroup group = df.desktopGroup();
957 group.writeEntry("Icon", KProtocolInfo::icon(linkUrl.protocol()));
958 group.writePathEntry("URL", linkUrl.prettyUrl());
959 df.sync();
960
961 m_copyData.m_src = tempFileName;
962 m_copyData.m_tempFileToDelete = tempFileName;
963
964 executeStrategy();
965}
966
967KNewFileMenu::KNewFileMenu(KActionCollection* collection, const QString& name, QObject* parent)
968 : KActionMenu(KIcon("document-new"), i18n("Create New"), parent),
969 d(new KNewFileMenuPrivate(this))
970{
971 // Don't fill the menu yet
972 // We'll do that in checkUpToDate (should be connected to aboutToShow)
973 d->m_newMenuGroup = new QActionGroup(this);
974 connect(d->m_newMenuGroup, SIGNAL(triggered(QAction*)), this, SLOT(_k_slotActionTriggered(QAction*)));
975 d->m_actionCollection = collection;
976 d->m_parentWidget = qobject_cast<QWidget*>(parent);
977 d->m_newDirAction = 0;
978
979 d->m_actionCollection->addAction(name, this);
980
981 d->m_menuDev = new KActionMenu(KIcon("drive-removable-media"), i18n("Link to Device"), this);
982}
983
984KNewFileMenu::~KNewFileMenu()
985{
986 //kDebug(1203) << this;
987 delete d;
988}
989
990void KNewFileMenu::checkUpToDate()
991{
992 KNewFileMenuSingleton* s = kNewMenuGlobals;
993 //kDebug(1203) << this << "m_menuItemsVersion=" << d->m_menuItemsVersion
994 // << "s->templatesVersion=" << s->templatesVersion;
995 if (d->m_menuItemsVersion < s->templatesVersion || s->templatesVersion == 0) {
996 //kDebug(1203) << "recreating actions";
997 // We need to clean up the action collection
998 // We look for our actions using the group
999 foreach (QAction* action, d->m_newMenuGroup->actions())
1000 delete action;
1001
1002 if (!s->templatesList) { // No templates list up to now
1003 s->templatesList = new KNewFileMenuSingleton::EntryList;
1004 d->_k_slotFillTemplates();
1005 s->parseFiles();
1006 }
1007
1008 // This might have been already done for other popupmenus,
1009 // that's the point in s->filesParsed.
1010 if (!s->filesParsed) {
1011 s->parseFiles();
1012 }
1013
1014 d->fillMenu();
1015
1016 d->m_menuItemsVersion = s->templatesVersion;
1017 }
1018}
1019
1020void KNewFileMenu::createDirectory()
1021{
1022 if (d->m_popupFiles.isEmpty())
1023 return;
1024
1025 KUrl baseUrl = d->m_popupFiles.first();
1026 QString name = d->m_text.isEmpty()? i18nc("Default name for a new folder", "New Folder") :
1027 d->m_text;
1028
1029 if (baseUrl.isLocalFile() && QFileInfo(baseUrl.toLocalFile(KUrl::AddTrailingSlash) + name).exists())
1030 name = KIO::RenameDialog::suggestName(baseUrl, name);
1031
1032 KDialog* fileDialog = new KDialog(d->m_parentWidget);
1033 fileDialog->setModal(isModal());
1034 fileDialog->setAttribute(Qt::WA_DeleteOnClose);
1035 fileDialog->setButtons(KDialog::Ok | KDialog::Cancel);
1036 fileDialog->setCaption(i18nc("@title:window", "New Folder"));
1037
1038 QWidget* mainWidget = new QWidget(fileDialog);
1039 QVBoxLayout *layout = new QVBoxLayout(mainWidget);
1040 QLabel *label = new QLabel(i18n("Create new folder in:\n%1", baseUrl.pathOrUrl()));
1041
1042 // We don't set the text of lineEdit in its constructor because the clear button would not be shown then.
1043 // It seems that setClearButtonShown(true) must be called *before* the text is set to make it work.
1044 // TODO: should probably be investigated and fixed in KLineEdit.
1045 KLineEdit *lineEdit = new KLineEdit;
1046 lineEdit->setClearButtonShown(true);
1047 lineEdit->setText(name);
1048
1049 d->_k_slotTextChanged(name); // have to save string in d->m_text in case user does not touch dialog
1050 connect(lineEdit, SIGNAL(textChanged(QString)), this, SLOT(_k_slotTextChanged(QString)));
1051 layout->addWidget(label);
1052 layout->addWidget(lineEdit);
1053
1054 fileDialog->setMainWidget(mainWidget);
1055 connect(fileDialog, SIGNAL(accepted()), this, SLOT(_k_slotCreateDirectory()));
1056 connect(fileDialog, SIGNAL(rejected()), this, SLOT(_k_slotAbortDialog()));
1057
1058 d->m_fileDialog = fileDialog;
1059
1060 fileDialog->show();
1061 lineEdit->selectAll();
1062 lineEdit->setFocus();
1063}
1064
1065bool KNewFileMenu::isModal() const
1066{
1067 return d->m_modal;
1068}
1069
1070KUrl::List KNewFileMenu::popupFiles() const
1071{
1072 return d->m_popupFiles;
1073}
1074
1075void KNewFileMenu::setModal(bool modal)
1076{
1077 d->m_modal = modal;
1078}
1079
1080void KNewFileMenu::setPopupFiles(const KUrl::List& files)
1081{
1082 d->m_popupFiles = files;
1083 if (files.isEmpty()) {
1084 d->m_newMenuGroup->setEnabled(false);
1085 } else {
1086 KUrl firstUrl = files.first();
1087 if (KProtocolManager::supportsWriting(firstUrl)) {
1088 d->m_newMenuGroup->setEnabled(true);
1089 if (d->m_newDirAction) {
1090 d->m_newDirAction->setEnabled(KProtocolManager::supportsMakeDir(firstUrl)); // e.g. trash:/
1091 }
1092 } else {
1093 d->m_newMenuGroup->setEnabled(true);
1094 }
1095 }
1096}
1097
1098
1099void KNewFileMenu::setParentWidget(QWidget* parentWidget)
1100{
1101 d->m_parentWidget = parentWidget;
1102}
1103
1104void KNewFileMenu::setSupportedMimeTypes(const QStringList& mime)
1105{
1106 d->m_supportedMimeTypes = mime;
1107}
1108
1109void KNewFileMenu::setViewShowsHiddenFiles(bool b)
1110{
1111 d->m_viewShowsHiddenFiles = b;
1112}
1113
1114void KNewFileMenu::slotResult(KJob * job)
1115{
1116 if (job->error()) {
1117 static_cast<KIO::Job*>(job)->ui()->showErrorMessage();
1118 } else {
1119 // Was this a copy or a mkdir?
1120 KIO::CopyJob* copyJob = ::qobject_cast<KIO::CopyJob*>(job);
1121 if (copyJob) {
1122 const KUrl destUrl = copyJob->destUrl();
1123 const KUrl localUrl = KIO::NetAccess::mostLocalUrl(destUrl, d->m_parentWidget);
1124 if (localUrl.isLocalFile()) {
1125 // Normal (local) file. Need to "touch" it, kio_file copied the mtime.
1126 (void) ::utime(QFile::encodeName(localUrl.toLocalFile()), 0);
1127 }
1128 emit fileCreated(destUrl);
1129 } else if (KIO::SimpleJob* simpleJob = ::qobject_cast<KIO::SimpleJob*>(job)) {
1130 // Can be mkdir or symlink
1131 if (simpleJob->property("isMkdirJob").toBool() == true) {
1132 kDebug() << "Emit directoryCreated" << simpleJob->url();
1133 emit directoryCreated(simpleJob->url());
1134 } else {
1135 emit fileCreated(simpleJob->url());
1136 }
1137 }
1138 }
1139 if (!d->m_tempFileToDelete.isEmpty())
1140 QFile::remove(d->m_tempFileToDelete);
1141}
1142
1143
1144QStringList KNewFileMenu::supportedMimeTypes() const
1145{
1146 return d->m_supportedMimeTypes;
1147}
1148
1149
1150#include "knewfilemenu.moc"
1151
1152