2 * KFontInst - KDE Font Installer
4 * Copyright 2003-2007 Craig Drummond <craig@kde.org>
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"
27 #include "JobRunner.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>
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
;
66 CDuplicatesDialog::CDuplicatesDialog(QWidget
*parent
, CJobRunner
*jr
, CFontList
*fl
)
67 : CActionDialog(parent
),
68 itsModifiedSys(false),
69 itsModifiedUser(false),
73 setCaption(i18n("Duplicate Fonts"));
74 setButtons(KDialog::Ok
|KDialog::Cancel
);
75 enableButtonOk(false);
77 QFrame
*page
= new QFrame(this);
80 QGridLayout
*layout
=new QGridLayout(page
);
82 layout
->setSpacing(KDialog::spacingHint());
83 itsLabel
=new QLabel(page
);
84 itsView
=new CFontFileListView(page
);
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()
107 if(itsFontFileList
->wasTerminated())
109 itsFontFileList
->wait();
114 CFontFileList::TFontMap duplicates
;
116 itsFontFileList
->getDuplicateFonts(duplicates
);
118 if(0==duplicates
.count())
119 itsLabel
->setText(i18n("No duplicate fonts found."));
122 QSize
sizeB4(size());
124 itsLabel
->setText(i18n("%1 duplicate fonts found.", duplicates
.count()));
127 CFontFileList::TFontMap::ConstIterator
it(duplicates
.begin()),
128 end(duplicates
.end());
129 QFont
boldFont(font());
131 boldFont
.setBold(true);
137 details
<< FC::createName(it
.key().family
, it
.key().styleInfo
);
139 QTreeWidgetItem
*top
=new QTreeWidgetItem(itsView
, details
);
141 QStringList::ConstIterator
fit((*it
).begin()),
146 for(; fit
!=fend
; ++fit
)
148 QFileInfo
info(*fit
);
150 details
.append(*fit
);
152 details
.append(KGlobal::locale()->formatByteSize(info
.size()));
153 details
.append(KGlobal::locale()->formatDateTime(info
.created()));
155 details
.append(info
.readLink());
156 new QTreeWidgetItem(top
, details
);
157 if(Misc::checkExt(*fit
, "pfa") || Misc::checkExt(*fit
, "pfb"))
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
);
194 STATUS_USER_CANCELLED
197 void CDuplicatesDialog::slotButtonClicked(int button
)
203 switch(deleteFiles())
205 case STATUS_NO_FILES
:
206 case STATUS_ALL_REMOVED
:
211 QList
<QString
> files
=itsView
->getMarkedFiles().toList();
214 KMessageBox::error(this, i18n("Could not delete:\n%1", files
.first()));
216 KMessageBox::errorList(this, i18n("Could not delete the following files:"), files
);
220 case STATUS_USER_CANCELLED
:
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();
249 int CDuplicatesDialog::deleteFiles()
251 QSet
<QString
> files(itsView
->getMarkedFiles());
254 return STATUS_NO_FILES
;
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
;
268 QSet
<QString
>::ConstIterator
it(files
.begin()),
270 QString
home(Misc::dirSyntax(QDir::homePath()));
273 if(Misc::root() || 0==(*it
).indexOf(home
))
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()),
286 QStringList delFiles
;
290 delFiles
.append(*it
);
292 if(constSysDelFiles
==delFiles
.size())
294 removed
+=deleteSysFiles(delFiles
);
300 removed
+=deleteSysFiles(delFiles
);
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()),
319 if(0==::unlink(QFile::encodeName(*it
).data()) || !Misc::fExists(*it
))
323 itsModifiedUser
=true;
328 QSet
<QString
> CDuplicatesDialog::deleteSysFiles(const QStringList
&files
)
330 QSet
<QString
> removed
;
334 QByteArray
cmd("rm -f");
335 QStringList::ConstIterator
it(files
.begin()),
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
))
359 static uint
qHash(const CFontFileList::TFile
&key
)
361 return qHash(key
.name
.toLower());
364 CFontFileList::CFontFileList(CDuplicatesDialog
*parent
)
370 void CFontFileList::start()
379 void CFontFileList::terminate()
384 void CFontFileList::getDuplicateFonts(TFontMap
&map
)
390 TFontMap::Iterator
it(map
.begin()),
393 // Now re-iterate, and remove any entries that only have 1 file...
394 for(it
=map
.begin(); it
!=end
; )
402 void CFontFileList::run()
404 const QList
<CFamilyItem
*> &families(((CDuplicatesDialog
*)parent())->fontList()->families());
405 QList
<CFamilyItem
*>::ConstIterator
it(families
.begin()),
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()),
433 QHash
<QString
, QSet
<TFile
> > folderMap
;
435 for(int n
=0; it
!=end
&& !itsTerminated
; ++it
)
438 QStringList::Iterator
fIt((*it
).begin()),
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
);
456 void CFontFileList::fileDuplicates(const QString
&folder
, const QSet
<TFile
> &files
)
459 QStringList nameFilters
;
460 QSet
<TFile
>::ConstIterator
it(files
.begin()),
463 // Filter the QDir to look for filenames matching (caselessly) to those in
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
)
509 headers
.append(i18n("Font/File"));
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..."),
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();
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
);
554 files
.insert(file
->text(0));
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());
584 if(0==font
->childCount())
585 removeFonts
.append(font
);
588 QList
<QTreeWidgetItem
*>::ConstIterator
it(removeFonts
.begin()),
589 end(removeFonts
.end());
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
;
604 if(item
->parent()) // Then its a file, not font name :-)
605 files
.insert(item
->text(0));
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()),
620 QProcess::startDetached(KFI_VIEWER
, args
);
625 void CFontFileListView::properties()
627 QList
<QTreeWidgetItem
*> items(selectedItems());
628 QTreeWidgetItem
*item
;
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
));
639 KPropertiesDialog
dlg(files
, this);
644 void CFontFileListView::mark()
646 QList
<QTreeWidgetItem
*> items(selectedItems());
647 QTreeWidgetItem
*item
;
655 void CFontFileListView::unmark()
657 QList
<QTreeWidgetItem
*> items(selectedItems());
658 QTreeWidgetItem
*item
;
666 void CFontFileListView::selectionChanged()
668 QList
<QTreeWidgetItem
*> items(selectedItems());
669 QTreeWidgetItem
*item
;
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())
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),
700 QList
<QTreeWidgetItem
*> items(selectedItems());
701 QTreeWidgetItem
*item
;
705 if(item
->parent() && item
->isSelected())
711 if(haveUnmarked
&& haveMaked
)
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());
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
))
746 emit
haveDeletions(true);
749 emit
haveDeletions(false);
754 #include "DuplicatesDialog.moc"