1/* This file is part of the KDE project
2 Copyright (C) 2000 - 2008 David Faure <faure@kde.org>
3 Copyright (C) 2008 Urs Wolfer <uwolfer @ kde.org>
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License or ( at
8 your option ) version 3 or, at the discretion of KDE e.V. ( which shall
9 act as a proxy as in section 14 of the GPLv3 ), any later version.
10
11 This program is distributed in the hope that it will be useful,
12 but WITHOUT ANY WARRANTY; without even the implied warranty of
13 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 General Public License for more details.
15
16 You should have received a copy of the GNU General Public License
17 along with this program; see the file COPYING. If not, write to
18 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
19 Boston, MA 02110-1301, USA.
20*/
21
22// Own
23#include "filetypesview.h"
24#include "mimetypewriter.h"
25
26// Qt
27#include <QLabel>
28#include <QLayout>
29#include <QtCore/QTimer>
30#include <QBoxLayout>
31#include <qdbusconnection.h>
32#include <qdbusmessage.h>
33
34// KDE
35#include <kbuildsycocaprogressdialog.h>
36#include <kdebug.h>
37#include <klineedit.h>
38#include <klocale.h>
39#include <kpushbutton.h>
40#include <kservicetypeprofile.h>
41#include <kstandarddirs.h>
42#include <kicon.h>
43#include <ksycoca.h>
44
45// Local
46#include "newtypedlg.h"
47#include "filetypedetails.h"
48#include "filegroupdetails.h"
49
50
51K_PLUGIN_FACTORY(FileTypesViewFactory, registerPlugin<FileTypesView>();)
52K_EXPORT_PLUGIN(FileTypesViewFactory("filetypes"))
53
54
55FileTypesView::FileTypesView(QWidget *parent, const QVariantList &)
56 : KCModule(FileTypesViewFactory::componentData(), parent)
57{
58 m_fileTypesConfig = KSharedConfig::openConfig("filetypesrc", KConfig::NoGlobals);
59
60 setQuickHelp( i18n("<p><h1>File Associations</h1>"
61 " This module allows you to choose which applications are associated"
62 " with a given type of file. File types are also referred to as MIME types"
63 " (MIME is an acronym which stands for \"Multipurpose Internet Mail"
64 " Extensions\").</p><p> A file association consists of the following:"
65 " <ul><li>Rules for determining the MIME-type of a file, for example"
66 " the filename pattern *.png, which means 'all files with names that end"
67 " in .png', is associated with the MIME type \"image/png\";</li>"
68 " <li>A short description of the MIME-type, for example the description"
69 " of the MIME type \"image/png\" is simply 'PNG image';</li>"
70 " <li>An icon to be used for displaying files of the given MIME-type,"
71 " so that you can easily identify the type of file in a file"
72 " manager or file-selection dialog (at least for the types you use often);</li>"
73 " <li>A list of the applications which can be used to open files of the"
74 " given MIME-type -- if more than one application can be used then the"
75 " list is ordered by priority.</li></ul>"
76 " You may be surprised to find that some MIME types have no associated"
77 " filename patterns; in these cases, KDE is able to determine the"
78 " MIME-type by directly examining the contents of the file.</p>"));
79
80 KServiceTypeProfile::setConfigurationMode();
81 setButtons(Help | Apply);
82 QString wtstr;
83
84 QHBoxLayout* l = new QHBoxLayout(this);
85 QVBoxLayout* leftLayout = new QVBoxLayout();
86 l->addLayout( leftLayout );
87
88 patternFilterLE = new KLineEdit(this);
89 patternFilterLE->setClearButtonShown(true);
90 patternFilterLE->setTrapReturnKey(true);
91 patternFilterLE->setClickMessage(i18n("Find file type or filename pattern"));
92 leftLayout->addWidget(patternFilterLE);
93
94 connect(patternFilterLE, SIGNAL(textChanged(const QString &)),
95 this, SLOT(slotFilter(const QString &)));
96
97 wtstr = i18n("Enter a part of a filename pattern, and only file types with a "
98 "matching file pattern will appear in the list. Alternatively, enter "
99 "a part of a file type name as it appears in the list.");
100
101 patternFilterLE->setWhatsThis( wtstr );
102
103 typesLV = new TypesListTreeWidget(this);
104
105 typesLV->setHeaderLabel(i18n("Known Types"));
106 leftLayout->addWidget(typesLV);
107 connect(typesLV, SIGNAL(currentItemChanged(QTreeWidgetItem *, QTreeWidgetItem *)),
108 this, SLOT(updateDisplay(QTreeWidgetItem *)));
109 connect(typesLV, SIGNAL(itemDoubleClicked(QTreeWidgetItem *, int)),
110 this, SLOT(slotDoubleClicked(QTreeWidgetItem *)));
111
112 typesLV->setWhatsThis( i18n("Here you can see a hierarchical list of"
113 " the file types which are known on your system. Click on the '+' sign"
114 " to expand a category, or the '-' sign to collapse it. Select a file type"
115 " (e.g. text/html for HTML files) to view/edit the information for that"
116 " file type using the controls on the right.") );
117
118 QHBoxLayout* btnsLay = new QHBoxLayout();
119 leftLayout->addLayout(btnsLay);
120 btnsLay->addStretch(1);
121 KPushButton *addTypeB = new KPushButton(i18n("Add..."), this);
122 addTypeB->setIcon(KIcon("list-add"));
123 connect(addTypeB, SIGNAL(clicked()), SLOT(addType()));
124 btnsLay->addWidget(addTypeB);
125
126 addTypeB->setWhatsThis( i18n("Click here to add a new file type.") );
127
128 m_removeTypeB = new KPushButton(i18n("&Remove"), this);
129 m_removeTypeB->setIcon(KIcon("list-remove"));
130 connect(m_removeTypeB, SIGNAL(clicked()), SLOT(removeType()));
131 btnsLay->addWidget(m_removeTypeB);
132 m_removeTypeB->setEnabled(false);
133 m_removeButtonSaysRevert = false;
134
135 // For the right panel, prepare a widget stack
136 m_widgetStack = new QStackedWidget(this);
137
138 l->addWidget( m_widgetStack );
139
140 // File Type Details
141 m_details = new FileTypeDetails( m_widgetStack );
142 connect( m_details, SIGNAL( changed(bool) ),
143 this, SLOT( setDirty(bool) ) );
144 connect( m_details, SIGNAL( embedMajor(const QString &, bool &) ),
145 this, SLOT( slotEmbedMajor(const QString &, bool &)));
146 m_widgetStack->insertWidget( 1, m_details /*id*/ );
147
148 // File Group Details
149 m_groupDetails = new FileGroupDetails( m_widgetStack );
150 connect( m_groupDetails, SIGNAL( changed(bool) ),
151 this, SLOT( setDirty(bool) ) );
152 m_widgetStack->insertWidget( 2,m_groupDetails /*id*/ );
153
154 // Widget shown on startup
155 m_emptyWidget = new QLabel( i18n("Select a file type by name or by extension"), m_widgetStack);
156 m_emptyWidget->setAlignment( Qt::AlignCenter );
157 m_widgetStack->insertWidget( 3,m_emptyWidget );
158
159 m_widgetStack->setCurrentWidget( m_emptyWidget );
160
161 connect(KSycoca::self(), SIGNAL(databaseChanged(QStringList)), SLOT(slotDatabaseChanged(QStringList)));
162}
163
164FileTypesView::~FileTypesView()
165{
166}
167
168void FileTypesView::setDirty(bool state)
169{
170 emit changed(state);
171 m_dirty = state;
172}
173
174// To order the mimetype list
175static bool mimeTypeLessThan(const KMimeType::Ptr& m1, const KMimeType::Ptr& m2)
176{
177 return m1->name() < m2->name();
178}
179
180// Note that this method loses any newly-added (and not saved yet) mimetypes.
181// So this is really only for load().
182void FileTypesView::readFileTypes()
183{
184 typesLV->clear();
185 m_majorMap.clear();
186 m_itemList.clear();
187
188 KMimeType::List mimetypes = KMimeType::allMimeTypes();
189 qSort(mimetypes.begin(), mimetypes.end(), mimeTypeLessThan);
190 KMimeType::List::const_iterator it2(mimetypes.constBegin());
191 for (; it2 != mimetypes.constEnd(); ++it2) {
192 const QString mimetype = (*it2)->name();
193 const int index = mimetype.indexOf('/');
194 const QString maj = mimetype.left(index);
195 const QString min = mimetype.right(mimetype.length() - index+1);
196
197 TypesListItem* groupItem = m_majorMap.value(maj);
198 if ( !groupItem ) {
199 groupItem = new TypesListItem(typesLV, maj);
200 m_majorMap.insert(maj, groupItem);
201 }
202
203 TypesListItem *item = new TypesListItem(groupItem, (*it2));
204 m_itemList.append( item );
205 }
206 updateDisplay(0L);
207}
208
209void FileTypesView::slotEmbedMajor(const QString &major, bool &embed)
210{
211 TypesListItem *groupItem = m_majorMap.value(major);
212 if (!groupItem)
213 return;
214
215 embed = (groupItem->mimeTypeData().autoEmbed() == MimeTypeData::Yes);
216}
217
218void FileTypesView::slotFilter(const QString & patternFilter)
219{
220 for (int i = 0; i < typesLV->topLevelItemCount(); ++i) {
221 typesLV->topLevelItem(i)->setHidden(true);
222 }
223
224 // insert all items and their group that match the filter
225 Q_FOREACH(TypesListItem* it, m_itemList) {
226 const MimeTypeData& mimeTypeData = it->mimeTypeData();
227 if ( patternFilter.isEmpty() || mimeTypeData.matchesFilter(patternFilter) ) {
228 TypesListItem *group = m_majorMap.value( mimeTypeData.majorType() );
229 Q_ASSERT(group);
230 if (group) {
231 group->setHidden(false);
232 it->setHidden(false);
233 }
234 } else {
235 it->setHidden(true);
236 }
237 }
238}
239
240void FileTypesView::addType()
241{
242 const QStringList allGroups = m_majorMap.keys();
243
244 NewTypeDialog dialog(allGroups, this);
245
246 if (dialog.exec()) {
247 const QString newMimeType = dialog.group() + '/' + dialog.text();
248
249 QTreeWidgetItemIterator it(typesLV);
250
251 TypesListItem *group = m_majorMap.value(dialog.group());
252 if ( !group ) {
253 group = new TypesListItem(typesLV, dialog.group());
254 m_majorMap.insert(dialog.group(), group);
255 }
256
257 // find out if our group has been filtered out -> insert if necessary
258 QTreeWidgetItem *item = typesLV->topLevelItem(0);
259 bool insert = true;
260 while ( item ) {
261 if ( item == group ) {
262 insert = false;
263 break;
264 }
265 item = typesLV->itemBelow(item);
266 }
267 if ( insert )
268 typesLV->addTopLevelItem( group );
269
270 TypesListItem *tli = new TypesListItem(group, newMimeType);
271 m_itemList.append( tli );
272
273 group->setExpanded(true);
274 tli->setSelected(true);
275
276 setDirty(true);
277 }
278}
279
280void FileTypesView::removeType()
281{
282 TypesListItem *current = static_cast<TypesListItem *>(typesLV->currentItem());
283
284 if (!current) {
285 return;
286 }
287
288 const MimeTypeData& mimeTypeData = current->mimeTypeData();
289
290 // Can't delete groups nor essential mimetypes (but the button should be
291 // disabled already in these cases, so this is just extra safety).
292 if (mimeTypeData.isMeta() || mimeTypeData.isEssential()) {
293 return;
294 }
295
296 if (!mimeTypeData.isNew()) {
297 removedList.append(mimeTypeData.name());
298 }
299 if (m_removeButtonSaysRevert) {
300 // Nothing else to do for now, until saving
301 updateDisplay(current);
302 } else {
303 QTreeWidgetItem *li = typesLV->itemAbove(current);
304 if (!li)
305 li = typesLV->itemBelow(current);
306 if (!li)
307 li = current->parent();
308
309 current->parent()->takeChild(current->parent()->indexOfChild(current));
310 m_itemList.removeAll(current);
311 if (li) {
312 li->setSelected(true);
313 }
314 }
315 setDirty(true);
316}
317
318void FileTypesView::slotDoubleClicked(QTreeWidgetItem *item)
319{
320 if ( !item ) return;
321 item->setExpanded( !item->isExpanded() );
322}
323
324void FileTypesView::updateDisplay(QTreeWidgetItem *item)
325{
326 TypesListItem *tlitem = static_cast<TypesListItem *>(item);
327 updateRemoveButton(tlitem);
328
329 if (!item) {
330 m_widgetStack->setCurrentWidget(m_emptyWidget);
331 return;
332 }
333
334 const bool wasDirty = m_dirty;
335
336 MimeTypeData& mimeTypeData = tlitem->mimeTypeData();
337
338 if (mimeTypeData.isMeta()) { // is a group
339 m_widgetStack->setCurrentWidget(m_groupDetails);
340 m_groupDetails->setMimeTypeData(&mimeTypeData);
341 } else {
342 m_widgetStack->setCurrentWidget(m_details);
343 m_details->setMimeTypeData(&mimeTypeData);
344 }
345
346 // Updating the display indirectly called change(true)
347 if (!wasDirty) {
348 setDirty(false);
349 }
350}
351
352void FileTypesView::updateRemoveButton(TypesListItem* tlitem)
353{
354 bool canRemove = false;
355 m_removeButtonSaysRevert = false;
356
357 if (tlitem) {
358 const MimeTypeData& mimeTypeData = tlitem->mimeTypeData();
359 if (!mimeTypeData.isMeta() && !mimeTypeData.isEssential()) {
360 if (mimeTypeData.isNew()) {
361 canRemove = true;
362 } else {
363 // We can only remove mimetypes that we defined ourselves, not those from freedesktop.org
364 const QString mimeType = mimeTypeData.name();
365 kDebug() << mimeType << "hasDefinitionFile:" << MimeTypeWriter::hasDefinitionFile(mimeType);
366 if (MimeTypeWriter::hasDefinitionFile(mimeType)) {
367 canRemove = true;
368
369 // Is there a global definition for it?
370 const QStringList mimeFiles = KGlobal::dirs()->findAllResources( "xdgdata-mime", mimeType + ".xml" );
371 kDebug() << mimeFiles;
372 if (mimeFiles.count() >= 2 /*a local and a global*/) {
373 m_removeButtonSaysRevert = true;
374 kDebug() << removedList;
375 if (removedList.contains(mimeType)) {
376 canRemove = false; // already on the "to be reverted" list, user needs to save now
377 }
378 }
379 }
380 }
381 }
382 }
383
384 if (m_removeButtonSaysRevert) {
385 m_removeTypeB->setText(i18n("&Revert"));
386 m_removeTypeB->setToolTip(i18n("Revert this file type to its initial system-wide definition"));
387 m_removeTypeB->setWhatsThis(i18n("Click here to revert this file type to its initial system-wide definition, which undoes any changes made to the file type. Note that system-wide file types cannot be deleted. You can however empty their pattern list, to minimize the chances of them being used (but the file type determination from file contents can still end up using them)."));
388 } else {
389 m_removeTypeB->setText(i18n("&Remove"));
390 m_removeTypeB->setToolTip(i18n("Delete this file type definition completely"));
391 m_removeTypeB->setWhatsThis(i18n("Click here to delete this file type definition completely. This is only possible for user-defined file types. System-wide file types cannot be deleted. You can however empty their pattern list, to minimize the chances of them being used (but the file type determination from file contents can still end up using them)."));
392 }
393
394 m_removeTypeB->setEnabled(canRemove);
395}
396
397
398void FileTypesView::save()
399{
400 bool needUpdateMimeDb = false;
401 bool needUpdateSycoca = false;
402 bool didIt = false;
403 // first, remove those items which we are asked to remove.
404 Q_FOREACH(const QString& mime, removedList) {
405 MimeTypeWriter::removeOwnMimeType(mime);
406 didIt = true;
407 needUpdateMimeDb = true;
408 needUpdateSycoca = true; // remove offers for this mimetype
409 }
410 removedList.clear();
411
412 // now go through all entries and sync those which are dirty.
413 // don't use typesLV, it may be filtered
414 QMap<QString,TypesListItem*>::iterator it1 = m_majorMap.begin();
415 while ( it1 != m_majorMap.end() ) {
416 TypesListItem *tli = *it1;
417 if (tli->mimeTypeData().isDirty()) {
418 kDebug() << "Entry " << tli->name() << " is dirty. Saving.";
419 if (tli->mimeTypeData().sync())
420 needUpdateMimeDb = true;
421 didIt = true;
422 }
423 ++it1;
424 }
425 Q_FOREACH(TypesListItem* tli, m_itemList) {
426 if (tli->mimeTypeData().isDirty()) {
427 if (tli->mimeTypeData().isServiceListDirty())
428 needUpdateSycoca = true;
429 kDebug() << "Entry " << tli->name() << " is dirty. Saving.";
430 if (tli->mimeTypeData().sync())
431 needUpdateMimeDb = true;
432 didIt = true;
433 }
434 }
435
436 m_fileTypesConfig->sync();
437
438 setDirty(false);
439
440 if (needUpdateMimeDb) {
441 MimeTypeWriter::runUpdateMimeDatabase();
442 }
443 if (needUpdateSycoca) {
444 KBuildSycocaProgressDialog::rebuildKSycoca(this);
445 }
446
447 if (didIt) { // TODO make more specific: only if autoEmbed changed? Well, maybe this is useful for icon and glob changes too...
448 // Trigger reparseConfiguration of filetypesrc in konqueror
449 // TODO: the same for dolphin. Or we should probably define a global signal for this.
450 // Or a KGlobalSettings thing.
451 QDBusMessage message =
452 QDBusMessage::createSignal("/KonqMain", "org.kde.Konqueror.Main", "reparseConfiguration");
453 QDBusConnection::sessionBus().send(message);
454 }
455
456 updateDisplay(typesLV->currentItem());
457}
458
459void FileTypesView::load()
460{
461 setEnabled(false);
462 setCursor( Qt::WaitCursor );
463
464 readFileTypes();
465
466 unsetCursor();
467 setDirty(false);
468 setEnabled(true);
469}
470
471void FileTypesView::slotDatabaseChanged(const QStringList& changedResources)
472{
473 kDebug() << changedResources;
474 if ( changedResources.contains("xdgdata-mime") // changes in mimetype definitions
475 || changedResources.contains("services") ) { // changes in .desktop files
476
477 m_details->refresh();
478
479 // ksycoca has new KMimeTypes objects for us, make sure to update
480 // our 'copies' to be in sync with it. Not important for OK, but
481 // important for Apply (how to differentiate those 2?).
482 // See BR 35071.
483
484 Q_FOREACH(TypesListItem* tli, m_itemList) {
485 tli->mimeTypeData().refresh();
486 }
487 }
488}
489
490void FileTypesView::defaults()
491{
492}
493
494#include "filetypesview.moc"
495
496
497