Make a branch to make krunner Good Enough For Aaron™.
[kdebase/uwolfer.git] / workspace / kcontrol / kfontinst / kcmfontinst / DuplicatesDialog.cpp
blobda278ccddbf475f14be072c5a77fe8ea3889184a
1 /*
2 * KFontInst - KDE Font Installer
4 * Copyright 2003-2007 Craig Drummond <craig@kde.org>
6 * ----
8 * This program is free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * This program is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * General Public License for more details.
18 * You should have received a copy of the GNU General Public License
19 * along with this program; see the file COPYING. If not, write to
20 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 * Boston, MA 02110-1301, USA.
24 #include "DuplicatesDialog.h"
25 #include "Misc.h"
26 #include "Fc.h"
27 #include "JobRunner.h"
28 #include "FontList.h"
29 #include <KDE/KGlobal>
30 #include <KDE/KIconLoader>
31 #include <KDE/KLocale>
32 #include <KDE/KMessageBox>
33 #include <KDE/SuProcess>
34 #include <KDE/KFileItem>
35 #include <KDE/KPropertiesDialog>
36 #include <KDE/KShell>
37 #include <QtGui/QLabel>
38 #include <QtCore/QTimer>
39 #include <QtGui/QGridLayout>
40 #include <QtCore/QDir>
41 #include <QtCore/QFileInfoList>
42 #include <QtCore/QFileInfo>
43 #include <QtGui/QHeaderView>
44 #include <QtCore/QDateTime>
45 #include <QtGui/QMenu>
46 #include <QtGui/QContextMenuEvent>
47 #include <QtGui/QAction>
48 #include <QtGui/QApplication>
49 #include <QtGui/QDesktopWidget>
50 #include <QtCore/QProcess>
52 using namespace KDESu;
54 namespace KFI
57 enum EDialogColumns
59 COL_FILE,
60 COL_TRASH,
61 COL_SIZE,
62 COL_DATE,
63 COL_LINK
66 CDuplicatesDialog::CDuplicatesDialog(QWidget *parent, CJobRunner *jr, CFontList *fl)
67 : CActionDialog(parent),
68 itsModifiedSys(false),
69 itsModifiedUser(false),
70 itsRunner(jr),
71 itsFontList(fl)
73 setCaption(i18n("Duplicate Fonts"));
74 setButtons(KDialog::Ok|KDialog::Cancel);
75 enableButtonOk(false);
77 QFrame *page = new QFrame(this);
78 setMainWidget(page);
80 QGridLayout *layout=new QGridLayout(page);
81 layout->setMargin(0);
82 layout->setSpacing(KDialog::spacingHint());
83 itsLabel=new QLabel(page);
84 itsView=new CFontFileListView(page);
85 itsView->hide();
86 layout->addWidget(itsPixmapLabel, 0, 0);
87 layout->addWidget(itsLabel, 0, 1);
88 itsLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
89 layout->addWidget(itsView, 1, 0, 1, 2);
90 itsFontFileList=new CFontFileList(this);
91 connect(itsFontFileList, SIGNAL(finished()), SLOT(scanFinished()));
92 connect(itsView, SIGNAL(haveDeletions(bool)), SLOT(enableButtonOk(bool)));
95 int CDuplicatesDialog::exec()
97 itsModifiedSys=itsModifiedUser=false;
98 itsLabel->setText(i18n("Scanning for duplicate fonts. Please wait..."));
99 itsFontFileList->start();
100 return CActionDialog::exec();
103 void CDuplicatesDialog::scanFinished()
105 stopAnimation();
107 if(itsFontFileList->wasTerminated())
109 itsFontFileList->wait();
110 reject();
112 else
114 CFontFileList::TFontMap duplicates;
116 itsFontFileList->getDuplicateFonts(duplicates);
118 if(0==duplicates.count())
119 itsLabel->setText(i18n("No duplicate fonts found."));
120 else
122 QSize sizeB4(size());
124 itsLabel->setText(i18n("%1 duplicate fonts found.", duplicates.count()));
125 itsView->show();
127 CFontFileList::TFontMap::ConstIterator it(duplicates.begin()),
128 end(duplicates.end());
129 QFont boldFont(font());
131 boldFont.setBold(true);
133 for(; it!=end; ++it)
135 QStringList details;
137 details << FC::createName(it.key().family, it.key().styleInfo);
139 QTreeWidgetItem *top=new QTreeWidgetItem(itsView, details);
141 QStringList::ConstIterator fit((*it).begin()),
142 fend((*it).end());
143 int tt(0),
144 t1(0);
146 for(; fit!=fend; ++fit)
148 QFileInfo info(*fit);
149 details.clear();
150 details.append(*fit);
151 details.append("");
152 details.append(KGlobal::locale()->formatByteSize(info.size()));
153 details.append(KGlobal::locale()->formatDateTime(info.created()));
154 if(info.isSymLink())
155 details.append(info.readLink());
156 new QTreeWidgetItem(top, details);
157 if(Misc::checkExt(*fit, "pfa") || Misc::checkExt(*fit, "pfb"))
158 t1++;
159 else
160 tt++;
162 top->setExpanded(true);
163 top->setData(COL_FILE, Qt::DecorationRole,
164 QVariant(SmallIcon(t1>tt ? "application-x-font-type1" : "application-x-font-ttf")));
165 top->setFont(COL_FILE, boldFont);
167 itsView->setSortingEnabled(true);
168 itsView->header()->resizeSections(QHeaderView::ResizeToContents);
170 int width=(KDialog::marginHint()+itsView->frameWidth()+8)*2;
172 for(int i=0; i<itsView->header()->count(); ++i)
173 width+=itsView->header()->sectionSize(i);
175 width=qMin(QApplication::desktop()->screenGeometry(this).width(), width);
176 resize(width, height());
177 QSize sizeNow(size());
178 if(sizeNow.width()>sizeB4.width())
180 int xmod=(sizeNow.width()-sizeB4.width())/2,
181 ymod=(sizeNow.height()-sizeB4.height())/2;
183 move(pos().x()-xmod, pos().y()-ymod);
189 enum EStatus
191 STATUS_NO_FILES,
192 STATUS_ALL_REMOVED,
193 STATUS_ERROR,
194 STATUS_USER_CANCELLED
197 void CDuplicatesDialog::slotButtonClicked(int button)
199 switch(button)
201 case KDialog::Ok:
203 switch(deleteFiles())
205 case STATUS_NO_FILES:
206 case STATUS_ALL_REMOVED:
207 accept();
208 break;
209 case STATUS_ERROR:
211 QList<QString> files=itsView->getMarkedFiles().toList();
213 if(1==files.count())
214 KMessageBox::error(this, i18n("Could not delete:\n%1", files.first()));
215 else
216 KMessageBox::errorList(this, i18n("Could not delete the following files:"), files);
217 break;
219 default:
220 case STATUS_USER_CANCELLED:
221 break;
223 break;
225 case KDialog::Cancel:
226 if(!itsFontFileList->wasTerminated())
228 if(itsFontFileList->isRunning())
230 if(KMessageBox::Yes==KMessageBox::warningYesNo(this, i18n("Abort font scan?")))
232 itsLabel->setText("Aborting...");
234 if(itsFontFileList->isRunning())
235 itsFontFileList->terminate();
236 else
237 reject();
240 else
241 reject();
243 break;
244 default:
245 break;
249 int CDuplicatesDialog::deleteFiles()
251 QSet<QString> files(itsView->getMarkedFiles());
253 if(!files.count())
254 return STATUS_NO_FILES;
256 if(1==files.count()
257 ? KMessageBox::Yes==KMessageBox::warningYesNo(this,
258 i18n("Are you sure you wish to delete:\n%1", files.toList().first()))
259 : KMessageBox::Yes==KMessageBox::warningYesNoList(this,
260 i18n("Are you sure you wish to delete:"), files.toList()))
262 QSet<QString> removed;
264 if(!Misc::root())
266 QSet<QString> sys,
267 user;
268 QSet<QString>::ConstIterator it(files.begin()),
269 end(files.end());
270 QString home(Misc::dirSyntax(QDir::homePath()));
272 for(; it!=end; ++it)
273 if(Misc::root() || 0==(*it).indexOf(home))
274 user.insert(*it);
275 else
276 sys.insert(*it);
278 if(user.count())
279 removed=deleteFiles(user);
280 if(sys.count() && itsRunner->getAdminPasswd(this))
282 static const int constSysDelFiles=16; // Number of files to rm -f in one go...
284 QSet<QString>::ConstIterator it(files.begin()),
285 end(files.end());
286 QStringList delFiles;
288 for(; it!=end; ++it)
290 delFiles.append(*it);
292 if(constSysDelFiles==delFiles.size())
294 removed+=deleteSysFiles(delFiles);
295 delFiles.clear();
299 if(delFiles.count())
300 removed+=deleteSysFiles(delFiles);
303 else
304 removed=deleteFiles(files);
306 itsView->removeFiles(removed);
307 return 0==itsView->getMarkedFiles().count() ? STATUS_ALL_REMOVED : STATUS_ERROR;
309 return STATUS_USER_CANCELLED;
312 QSet<QString> CDuplicatesDialog::deleteFiles(const QSet<QString> &files)
314 QSet<QString> removed;
315 QSet<QString>::ConstIterator it(files.begin()),
316 end(files.end());
318 for(; it!=end; ++it)
319 if(0==::unlink(QFile::encodeName(*it).data()) || !Misc::fExists(*it))
320 removed.insert(*it);
322 if(removed.count())
323 itsModifiedUser=true;
325 return removed;
328 QSet<QString> CDuplicatesDialog::deleteSysFiles(const QStringList &files)
330 QSet<QString> removed;
332 if(files.count())
334 QByteArray cmd("rm -f");
335 QStringList::ConstIterator it(files.begin()),
336 end(files.end());
338 for(; it!=end; ++it)
340 cmd+=' ';
341 cmd+=QFile::encodeName(KShell::quoteArg(*it));
344 SuProcess proc(KFI_SYS_USER);
346 proc.setCommand(cmd);
347 proc.exec(itsRunner->adminPasswd().toLocal8Bit());
349 for(it=files.begin(); it!=end; ++it)
350 if(!Misc::fExists(*it))
351 removed.insert(*it);
354 if(removed.count())
355 itsModifiedSys=true;
356 return removed;
359 static uint qHash(const CFontFileList::TFile &key)
361 return qHash(key.name.toLower());
364 CFontFileList::CFontFileList(CDuplicatesDialog *parent)
365 : QThread(parent),
366 itsTerminated(false)
370 void CFontFileList::start()
372 if(!isRunning())
374 itsTerminated=false;
375 QThread::start();
379 void CFontFileList::terminate()
381 itsTerminated=true;
384 void CFontFileList::getDuplicateFonts(TFontMap &map)
386 map=itsMap;
388 if(map.count())
390 TFontMap::Iterator it(map.begin()),
391 end(map.end());
393 // Now re-iterate, and remove any entries that only have 1 file...
394 for(it=map.begin(); it!=end; )
395 if((*it).count()<2)
396 it=map.erase(it);
397 else
398 ++it;
402 void CFontFileList::run()
404 const QList<CFamilyItem *> &families(((CDuplicatesDialog *)parent())->fontList()->families());
405 QList<CFamilyItem *>::ConstIterator it(families.begin()),
406 end(families.end());
408 for(; it!=end; ++it)
410 QList<CFontItem *>::ConstIterator fontIt((*it)->fonts().begin()),
411 fontEnd((*it)->fonts().end());
413 for(; fontIt!=fontEnd; ++fontIt)
414 if(!(*fontIt)->isBitmap())
416 Misc::TFont font((*fontIt)->family(), (*fontIt)->styleInfo());
417 CDisabledFonts::TFileList::ConstIterator fileIt((*fontIt)->files().begin()),
418 fileEnd((*fontIt)->files().end());
420 for(; fileIt!=fileEnd; ++fileIt)
421 if(!Misc::isMetrics(*fileIt))
422 itsMap[font].append(*fileIt);
426 // if we have 2 fonts: /wibble/a.ttf and /wibble/a.TTF fontconfig only returns the 1st, so we
427 // now iterate over fontconfig's list, and look for other matching fonts...
428 if(itsMap.count() && !itsTerminated)
430 // Create a map of folder -> set<files>
431 TFontMap::Iterator it(itsMap.begin()),
432 end(itsMap.end());
433 QHash<QString, QSet<TFile> > folderMap;
435 for(int n=0; it!=end && !itsTerminated; ++it)
437 QStringList add;
438 QStringList::Iterator fIt((*it).begin()),
439 fEnd((*it).end());
441 for(; fIt!=fEnd && !itsTerminated; ++fIt, ++n)
442 folderMap[Misc::getDir(*fIt)].insert(TFile(Misc::getFile(*fIt), it));
445 // Go through our folder map, and check for file duplicates...
446 QHash<QString, QSet<TFile> >::Iterator folderIt(folderMap.begin()),
447 folderEnd(folderMap.end());
449 for(; folderIt!=folderEnd && !itsTerminated; ++folderIt)
450 fileDuplicates(folderIt.key(), *folderIt);
453 emit finished();
456 void CFontFileList::fileDuplicates(const QString &folder, const QSet<TFile> &files)
458 QDir dir(folder);
459 QStringList nameFilters;
460 QSet<TFile>::ConstIterator it(files.begin()),
461 end(files.end());
463 // Filter the QDir to look for filenames matching (caselessly) to those in
464 // files...
465 for(; it!=end; ++it)
466 nameFilters.append((*it).name);
468 dir.setFilter(QDir::Files|QDir::Hidden);
469 dir.setNameFilters(nameFilters);
471 QFileInfoList list(dir.entryInfoList());
473 for (int i = 0; i < list.size() && !itsTerminated; ++i)
475 QFileInfo fileInfo(list.at(i));
477 // Check if this file is already know about - this will do a case-sensitive comparison
478 if(!files.contains(TFile(fileInfo.fileName())))
480 // OK, not found - this means its a duplicate, but different case. So, find the
481 // FontMap iterator, and update its list of files.
482 QSet<TFile>::ConstIterator entry=files.find(TFile(fileInfo.fileName(), true));
484 if(entry!=files.end())
485 (*((*entry).it)).append(fileInfo.absoluteFilePath());
490 inline void markItem(QTreeWidgetItem *item)
492 item->setData(COL_TRASH, Qt::DecorationRole, QVariant(SmallIcon("list-remove")));
495 inline void unmarkItem(QTreeWidgetItem *item)
497 item->setData(COL_TRASH, Qt::DecorationRole, QVariant());
500 inline bool isMarked(QTreeWidgetItem *item)
502 return item->data(COL_TRASH, Qt::DecorationRole).isValid();
505 CFontFileListView::CFontFileListView(QWidget *parent)
506 : QTreeWidget(parent)
508 QStringList headers;
509 headers.append(i18n("Font/File"));
510 headers.append("");
511 headers.append(i18n("Size"));
512 headers.append(i18n("Date"));
513 headers.append(i18n("Links To"));
514 setHeaderLabels(headers);
515 headerItem()->setData(COL_TRASH, Qt::DecorationRole, QVariant(SmallIcon("user-trash")));
516 setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding);
517 setSelectionMode(ExtendedSelection);
518 sortByColumn(COL_FILE, Qt::AscendingOrder);
519 setSelectionBehavior(SelectRows);
520 setSortingEnabled(true);
521 setAllColumnsShowFocus(true);
522 setAlternatingRowColors(true);
524 itsMenu=new QMenu(this);
525 itsMenu->addAction(KIcon("kfontview"), i18n("Open in Font Viewer"),
526 this, SLOT(openViewer()));
527 itsMenu->addAction(KIcon("document-properties"), i18n("Properties"),
528 this, SLOT(properties()));
529 itsMenu->addSeparator();
530 itsUnMarkAct=itsMenu->addAction(i18n("Unmark for Deletion..."),
531 this, SLOT(unmark()));
532 itsMarkAct=itsMenu->addAction(KIcon("edit-delete"), i18n("Mark for Deletion..."),
533 this, SLOT(mark()));
535 connect(this, SIGNAL(itemSelectionChanged()), SLOT(selectionChanged()));
536 connect(this, SIGNAL(itemClicked(QTreeWidgetItem *, int)), SLOT(clicked(QTreeWidgetItem *, int)));
539 QSet<QString> CFontFileListView::getMarkedFiles()
541 QTreeWidgetItem *root=invisibleRootItem();
542 QSet<QString> files;
544 for(int t=0; t<root->childCount(); ++t)
546 QList<QTreeWidgetItem *> removeFiles;
547 QTreeWidgetItem *font=root->child(t);
549 for(int c=0; c<font->childCount(); ++c)
551 QTreeWidgetItem *file=font->child(c);
553 if(isMarked(file))
554 files.insert(file->text(0));
558 return files;
561 void CFontFileListView::removeFiles(const QSet<QString> &files)
563 QTreeWidgetItem *root=invisibleRootItem();
564 QList<QTreeWidgetItem *> removeFonts;
566 for(int t=0; t<root->childCount(); ++t)
568 QList<QTreeWidgetItem *> removeFiles;
569 QTreeWidgetItem *font=root->child(t);
571 for(int c=0; c<font->childCount(); ++c)
573 QTreeWidgetItem *file=font->child(c);
575 if(files.contains(file->text(0)))
576 removeFiles.append(file);
579 QList<QTreeWidgetItem *>::ConstIterator it(removeFiles.begin()),
580 end(removeFiles.end());
582 for(; it!=end; ++it)
583 delete (*it);
584 if(0==font->childCount())
585 removeFonts.append(font);
588 QList<QTreeWidgetItem *>::ConstIterator it(removeFonts.begin()),
589 end(removeFonts.end());
590 for(; it!=end; ++it)
591 delete (*it);
594 void CFontFileListView::openViewer()
596 // Number of fonts user has selected, before we ask if they really want to view them all...
597 static const int constMaxBeforePrompt=10;
599 QList<QTreeWidgetItem *> items(selectedItems());
600 QTreeWidgetItem *item;
601 QSet<QString> files;
603 foreach(item, items)
604 if(item->parent()) // Then its a file, not font name :-)
605 files.insert(item->text(0));
607 if(files.count() &&
608 (files.count()<constMaxBeforePrompt ||
609 KMessageBox::Yes==KMessageBox::questionYesNo(this, i18n("Open all %1 fonts in font viewer?", files.count()))))
611 QSet<QString>::ConstIterator it(files.begin()),
612 end(files.end());
614 for(; it!=end; ++it)
616 QStringList args;
618 args << (*it);
620 QProcess::startDetached(KFI_VIEWER, args);
625 void CFontFileListView::properties()
627 QList<QTreeWidgetItem *> items(selectedItems());
628 QTreeWidgetItem *item;
629 KFileItemList files;
631 foreach(item, items)
632 if(item->parent())
633 files.append(KFileItem(KUrl::fromPath(item->text(0)),
634 KMimeType::findByPath(item->text(0))->name(),
635 item->text(COL_LINK).isEmpty() ? S_IFREG : S_IFLNK));
637 if(files.count())
639 KPropertiesDialog dlg(files, this);
640 dlg.exec();
644 void CFontFileListView::mark()
646 QList<QTreeWidgetItem *> items(selectedItems());
647 QTreeWidgetItem *item;
649 foreach(item, items)
650 if(item->parent())
651 markItem(item);
652 checkFiles();
655 void CFontFileListView::unmark()
657 QList<QTreeWidgetItem *> items(selectedItems());
658 QTreeWidgetItem *item;
660 foreach(item, items)
661 if(item->parent())
662 unmarkItem(item);
663 checkFiles();
666 void CFontFileListView::selectionChanged()
668 QList<QTreeWidgetItem *> items(selectedItems());
669 QTreeWidgetItem *item;
671 foreach(item, items)
672 if(!item->parent() && item->isSelected())
673 item->setSelected(false);
676 void CFontFileListView::clicked(QTreeWidgetItem *item, int col)
678 if(item && COL_TRASH==col && item->parent())
680 if(isMarked(item))
681 unmarkItem(item);
682 else
683 markItem(item);
684 checkFiles();
688 void CFontFileListView::contextMenuEvent(QContextMenuEvent *ev)
690 QTreeWidgetItem *item(itemAt(ev->pos()));
692 if(item && item->parent())
694 if(!item->isSelected())
695 item->setSelected(true);
697 bool haveUnmarked(false),
698 haveMaked(false);
700 QList<QTreeWidgetItem *> items(selectedItems());
701 QTreeWidgetItem *item;
703 foreach(item, items)
705 if(item->parent() && item->isSelected())
706 if(isMarked(item))
707 haveMaked=true;
708 else
709 haveUnmarked=true;
711 if(haveUnmarked && haveMaked)
712 break;
715 itsMarkAct->setEnabled(haveUnmarked);
716 itsUnMarkAct->setEnabled(haveMaked);
717 itsMenu->popup(ev->globalPos());
721 void CFontFileListView::checkFiles()
723 // Need to check that if we mark a file that is linked to, then we also need
724 // to mark the sym link.
725 QSet<QString> marked(getMarkedFiles());
727 if(marked.count())
729 QTreeWidgetItem *root=invisibleRootItem();
731 for(int t=0; t<root->childCount(); ++t)
733 QTreeWidgetItem *font=root->child(t);
735 for(int c=0; c<font->childCount(); ++c)
737 QTreeWidgetItem *file=font->child(c);
738 QString link(font->child(c)->text(COL_LINK));
740 if(!link.isEmpty() && marked.contains(link))
741 if(!isMarked(file))
742 markItem(file);
746 emit haveDeletions(true);
748 else
749 emit haveDeletions(false);
754 #include "DuplicatesDialog.moc"