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 | |
57 | static 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 |
72 | class |
73 | { |
74 | public: |
75 | () |
76 | : dirWatch(0), |
77 | filesParsed(false), |
78 | templatesList(0), |
79 | templatesVersion(0) |
80 | { |
81 | } |
82 | |
83 | () |
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 { , = 1, , }; |
104 | |
105 | KDirWatch * ; |
106 | |
107 | struct { |
108 | QString ; |
109 | QString ; // empty for Separator |
110 | QString ; // same as filePath for Template |
111 | QString ; |
112 | EntryType ; |
113 | QString ; |
114 | QString ; |
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> ; |
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 ; |
129 | EntryList * ; |
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 ; |
137 | }; |
138 | |
139 | void KNewFileMenuSingleton::() |
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 | |
207 | K_GLOBAL_STATIC(KNewFileMenuSingleton, ) |
208 | |
209 | class |
210 | { |
211 | public: |
212 | () { m_isSymlink = false;} |
213 | () {} |
214 | QString () const { return m_chosenFileName; } |
215 | |
216 | // If empty, no copy is performed. |
217 | QString () const { return m_src; } |
218 | QString () const { return m_tempFileToDelete; } |
219 | bool ; |
220 | |
221 | QString ; |
222 | QString ; |
223 | QString ; |
224 | QString ; |
225 | }; |
226 | |
227 | class |
228 | { |
229 | public: |
230 | (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 * ; |
332 | KDialog* ; |
333 | |
334 | KActionMenu *; |
335 | int ; |
336 | bool ; |
337 | QAction* ; |
338 | |
339 | /** |
340 | * The action group that our actions belong to |
341 | */ |
342 | QActionGroup* ; |
343 | QWidget *; |
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 ; |
350 | |
351 | QStringList ; |
352 | QString ; // set when a tempfile was created for a Type=URL desktop file |
353 | QString ; |
354 | bool ; |
355 | |
356 | KNewFileMenu* ; |
357 | |
358 | KNewFileMenuCopyData ; |
359 | }; |
360 | |
361 | bool KNewFileMenuPrivate::(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 | |
387 | void KNewFileMenuPrivate::(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 | |
421 | void KNewFileMenuPrivate::(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 | |
452 | void KNewFileMenuPrivate::(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 | |
497 | void KNewFileMenuPrivate::(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 | |
508 | void KNewFileMenuPrivate::() |
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 | |
566 | void KNewFileMenuPrivate::(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 | |
578 | void KNewFileMenuPrivate::() |
579 | { |
580 | QMenu* = 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 | |
711 | void KNewFileMenuPrivate::() |
712 | { |
713 | m_text = QString(); |
714 | } |
715 | |
716 | void KNewFileMenuPrivate::(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 | |
752 | void KNewFileMenuPrivate::(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 | |
795 | void KNewFileMenuPrivate::() |
796 | { |
797 | _k_slotCreateDirectory(true); |
798 | } |
799 | |
800 | void KNewFileMenuPrivate::() |
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 | |
854 | void KNewFileMenuPrivate::() |
855 | { |
856 | executeStrategy(); |
857 | } |
858 | |
859 | void KNewFileMenuPrivate::() |
860 | { |
861 | m_copyData.m_chosenFileName = m_text; |
862 | _k_slotAbortDialog(); |
863 | executeStrategy(); |
864 | } |
865 | |
866 | void KNewFileMenuPrivate::() |
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 | |
902 | void KNewFileMenuPrivate::(const QString & text) |
903 | { |
904 | m_text = text; |
905 | } |
906 | |
907 | void KNewFileMenuPrivate::() |
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 | |
967 | 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 | |
984 | KNewFileMenu::() |
985 | { |
986 | //kDebug(1203) << this; |
987 | delete d; |
988 | } |
989 | |
990 | void KNewFileMenu::() |
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 | |
1020 | void KNewFileMenu::() |
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 | |
1065 | bool KNewFileMenu::() const |
1066 | { |
1067 | return d->m_modal; |
1068 | } |
1069 | |
1070 | KUrl::List KNewFileMenu::() const |
1071 | { |
1072 | return d->m_popupFiles; |
1073 | } |
1074 | |
1075 | void KNewFileMenu::(bool modal) |
1076 | { |
1077 | d->m_modal = modal; |
1078 | } |
1079 | |
1080 | void KNewFileMenu::(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 | |
1099 | void KNewFileMenu::(QWidget* parentWidget) |
1100 | { |
1101 | d->m_parentWidget = parentWidget; |
1102 | } |
1103 | |
1104 | void KNewFileMenu::(const QStringList& mime) |
1105 | { |
1106 | d->m_supportedMimeTypes = mime; |
1107 | } |
1108 | |
1109 | void KNewFileMenu::(bool b) |
1110 | { |
1111 | d->m_viewShowsHiddenFiles = b; |
1112 | } |
1113 | |
1114 | void KNewFileMenu::(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 | |
1144 | QStringList KNewFileMenu::() const |
1145 | { |
1146 | return d->m_supportedMimeTypes; |
1147 | } |
1148 | |
1149 | |
1150 | #include "knewfilemenu.moc" |
1151 | |
1152 | |