/* * KFontInst - KDE Font Installer * * Copyright 2003-2007 Craig Drummond * * ---- * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include "DuplicatesDialog.h" #include "ActionLabel.h" #include "Misc.h" #include "Fc.h" #include "FcEngine.h" #include "FontList.h" #include "JobRunner.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined USE_POLICYKIT && USE_POLICYKIT==1 #include #endif using namespace KDESu; namespace KFI { enum EDialogColumns { COL_FILE, COL_TRASH, COL_SIZE, COL_DATE, COL_LINK }; CDuplicatesDialog::CDuplicatesDialog(QWidget *parent, CFontList *fl) : KDialog(parent), itsFontList(fl) { setCaption(i18n("Duplicate Fonts")); setButtons(Cancel); setModal(true); QFrame *page = new QFrame(this); setMainWidget(page); QGridLayout *layout=new QGridLayout(page); layout->setMargin(0); layout->setSpacing(KDialog::spacingHint()); itsLabel=new QLabel(page); itsView=new CFontFileListView(page); itsView->hide(); layout->addWidget(itsActionLabel = new CActionLabel(this), 0, 0); layout->addWidget(itsLabel, 0, 1); itsLabel->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); layout->addWidget(itsView, 1, 0, 1, 2); itsFontFileList=new CFontFileList(this); connect(itsFontFileList, SIGNAL(finished()), SLOT(scanFinished())); connect(itsView, SIGNAL(haveDeletions(bool)), SLOT(enableButtonOk(bool))); } int CDuplicatesDialog::exec() { itsActionLabel->startAnimation(); itsLabel->setText(i18n("Scanning for duplicate fonts. Please wait...")); itsFontFileList->start(); return KDialog::exec(); } void CDuplicatesDialog::scanFinished() { itsActionLabel->stopAnimation(); if(itsFontFileList->wasTerminated()) { itsFontFileList->wait(); reject(); } else { CFontFileList::TFontMap duplicates; itsFontFileList->getDuplicateFonts(duplicates); if(0==duplicates.count()) { setButtons(Close); itsLabel->setText(i18n("No duplicate fonts found.")); } else { QSize sizeB4(size()); setButtons(Ok|Close); setButtonText(Ok, i18n("Delete Marked Files")); enableButtonOk(false); itsLabel->setText(i18np("%1 duplicate font found.", "%1 duplicate fonts found.", duplicates.count())); itsView->show(); CFontFileList::TFontMap::ConstIterator it(duplicates.begin()), end(duplicates.end()); QFont boldFont(font()); boldFont.setBold(true); for(; it!=end; ++it) { QStringList details; details << FC::createName(it.key().family, it.key().styleInfo); CFontFileListView::StyleItem *top=new CFontFileListView::StyleItem(itsView, details, it.key().family, it.key().styleInfo); QSet::ConstIterator fit((*it).begin()), fend((*it).end()); int tt(0), t1(0); for(; fit!=fend; ++fit) { QFileInfo info(*fit); details.clear(); details.append(*fit); details.append(""); details.append(KGlobal::locale()->formatByteSize(info.size())); details.append(KGlobal::locale()->formatDateTime(info.created())); if(info.isSymLink()) details.append(info.readLink()); new QTreeWidgetItem(top, details); if(Misc::checkExt(*fit, "pfa") || Misc::checkExt(*fit, "pfb")) t1++; else tt++; } top->setData(COL_FILE, Qt::DecorationRole, QVariant(SmallIcon(t1>tt ? "application-x-font-type1" : "application-x-font-ttf"))); top->setFont(COL_FILE, boldFont); } QTreeWidgetItem *item=0L; for(int i=0; (item=itsView->topLevelItem(i)); ++i) item->setExpanded(true); itsView->setSortingEnabled(true); itsView->header()->resizeSections(QHeaderView::ResizeToContents); int width=(KDialog::marginHint()+itsView->frameWidth()+8)*2; for(int i=0; iheader()->count(); ++i) width+=itsView->header()->sectionSize(i); width=qMin(QApplication::desktop()->screenGeometry(this).width(), width); resize(width, height()); QSize sizeNow(size()); if(sizeNow.width()>sizeB4.width()) { int xmod=(sizeNow.width()-sizeB4.width())/2, ymod=(sizeNow.height()-sizeB4.height())/2; move(pos().x()-xmod, pos().y()-ymod); } } } } enum EStatus { STATUS_NO_FILES, STATUS_ALL_REMOVED, STATUS_ERROR, STATUS_USER_CANCELLED }; void CDuplicatesDialog::slotButtonClicked(int button) { switch(button) { case KDialog::Ok: { QSet files=itsView->getMarkedFiles(); int fCount=files.count(); if(1==fCount ? KMessageBox::Yes==KMessageBox::warningYesNo(this, i18n("Are you sure you wish to delete:\n%1", files.toList().first())) : KMessageBox::Yes==KMessageBox::warningYesNoList(this, i18n("Are you sure you wish to delete:"), files.toList())) { itsFontList->setSlowUpdates(true); CJobRunner runner(this); connect(&runner, SIGNAL(configuring()), itsFontList, SLOT(unsetSlowUpdates())); runner.exec(CJobRunner::CMD_REMOVE_FILE, itsView->getMarkedItems(), false); itsFontList->setSlowUpdates(false); itsView->removeFiles(); files=itsView->getMarkedFiles(); if(fCount!=files.count()) CFcEngine::setDirty(); if(0==files.count()) accept(); } break; } case KDialog::Cancel: case KDialog::Close: if(!itsFontFileList->wasTerminated()) { if(itsFontFileList->isRunning()) { if(KMessageBox::Yes==KMessageBox::warningYesNo(this, i18n("Cancel font scan?"))) { itsLabel->setText(i18n("Canceling...")); if(itsFontFileList->isRunning()) itsFontFileList->terminate(); else reject(); } } else reject(); } break; default: break; } } static uint qHash(const CFontFileList::TFile &key) { return qHash(key.name.toLower()); } CFontFileList::CFontFileList(CDuplicatesDialog *parent) : QThread(parent), itsTerminated(false) { } void CFontFileList::start() { if(!isRunning()) { itsTerminated=false; QThread::start(); } } void CFontFileList::terminate() { itsTerminated=true; } void CFontFileList::getDuplicateFonts(TFontMap &map) { map=itsMap; if(map.count()) { TFontMap::Iterator it(map.begin()), end(map.end()); // Now re-iterate, and remove any entries that only have 1 file... for(it=map.begin(); it!=end; ) if((*it).count()<2) it=map.erase(it); else ++it; } } void CFontFileList::run() { const QList &families(((CDuplicatesDialog *)parent())->fontList()->families()); QList::ConstIterator it(families.begin()), end(families.end()); for(; it!=end; ++it) { QList::ConstIterator fontIt((*it)->fonts().begin()), fontEnd((*it)->fonts().end()); for(; fontIt!=fontEnd; ++fontIt) if(!(*fontIt)->isBitmap()) { Misc::TFont font((*fontIt)->family(), (*fontIt)->styleInfo()); FileCont::ConstIterator fileIt((*fontIt)->files().begin()), fileEnd((*fontIt)->files().end()); for(; fileIt!=fileEnd; ++fileIt) if(!Misc::isMetrics((*fileIt).path()) && !Misc::isBitmap((*fileIt).path())) itsMap[font].insert((*fileIt).path()); } } // if we have 2 fonts: /wibble/a.ttf and /wibble/a.TTF fontconfig only returns the 1st, so we // now iterate over fontconfig's list, and look for other matching fonts... if(itsMap.count() && !itsTerminated) { // Create a map of folder -> set TFontMap::Iterator it(itsMap.begin()), end(itsMap.end()); QHash > folderMap; for(int n=0; it!=end && !itsTerminated; ++it) { QStringList add; QSet::const_iterator fIt((*it).begin()), fEnd((*it).end()); for(; fIt!=fEnd && !itsTerminated; ++fIt, ++n) folderMap[Misc::getDir(*fIt)].insert(TFile(Misc::getFile(*fIt), it)); } // Go through our folder map, and check for file duplicates... QHash >::Iterator folderIt(folderMap.begin()), folderEnd(folderMap.end()); for(; folderIt!=folderEnd && !itsTerminated; ++folderIt) fileDuplicates(folderIt.key(), *folderIt); } emit finished(); } void CFontFileList::fileDuplicates(const QString &folder, const QSet &files) { QDir dir(folder); dir.setFilter(QDir::Files|QDir::Hidden); QFileInfoList list(dir.entryInfoList()); for (int i = 0; i < list.size() && !itsTerminated; ++i) { QFileInfo fileInfo(list.at(i)); // Check if this file is already know about - this will do a case-sensitive comparison if(!files.contains(TFile(fileInfo.fileName()))) { // OK, not found - this means it is a duplicate, but different case. So, find the // FontMap iterator, and update its list of files. QSet::ConstIterator entry=files.find(TFile(fileInfo.fileName(), true)); if(entry!=files.end()) (*((*entry).it)).insert(fileInfo.absoluteFilePath()); } } } inline void markItem(QTreeWidgetItem *item) { item->setData(COL_TRASH, Qt::DecorationRole, QVariant(SmallIcon("list-remove"))); } inline void unmarkItem(QTreeWidgetItem *item) { item->setData(COL_TRASH, Qt::DecorationRole, QVariant()); } inline bool isMarked(QTreeWidgetItem *item) { return item->data(COL_TRASH, Qt::DecorationRole).isValid(); } CFontFileListView::CFontFileListView(QWidget *parent) : QTreeWidget(parent) { QStringList headers; headers.append(i18n("Font/File")); headers.append(""); headers.append(i18n("Size")); headers.append(i18n("Date")); headers.append(i18n("Links To")); setHeaderLabels(headers); headerItem()->setData(COL_TRASH, Qt::DecorationRole, QVariant(SmallIcon("user-trash"))); setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); setSelectionMode(ExtendedSelection); sortByColumn(COL_FILE, Qt::AscendingOrder); setSelectionBehavior(SelectRows); setSortingEnabled(true); setAllColumnsShowFocus(true); setAlternatingRowColors(true); itsMenu=new QMenu(this); if(!Misc::app(KFI_VIEWER).isEmpty()) itsMenu->addAction(KIcon("kfontview"), i18n("Open in Font Viewer"), this, SLOT(openViewer())); itsMenu->addAction(KIcon("document-properties"), i18n("Properties"), this, SLOT(properties())); itsMenu->addSeparator(); itsUnMarkAct=itsMenu->addAction(i18n("Unmark for Deletion"), this, SLOT(unmark())); itsMarkAct=itsMenu->addAction(KIcon("edit-delete"), i18n("Mark for Deletion"), this, SLOT(mark())); connect(this, SIGNAL(itemSelectionChanged()), SLOT(selectionChanged())); connect(this, SIGNAL(itemClicked(QTreeWidgetItem*,int)), SLOT(clicked(QTreeWidgetItem*,int))); } QSet CFontFileListView::getMarkedFiles() { QTreeWidgetItem *root=invisibleRootItem(); QSet files; for(int t=0; tchildCount(); ++t) { QList removeFiles; QTreeWidgetItem *font=root->child(t); for(int c=0; cchildCount(); ++c) { QTreeWidgetItem *file=font->child(c); if(isMarked(file)) files.insert(file->text(0)); } } return files; } CJobRunner::ItemList CFontFileListView::getMarkedItems() { QTreeWidgetItem *root=invisibleRootItem(); CJobRunner::ItemList items; QString home(Misc::dirSyntax(QDir::homePath())); for(int t=0; tchildCount(); ++t) { QList removeFiles; StyleItem *style=(StyleItem *)root->child(t); for(int c=0; cchildCount(); ++c) { QTreeWidgetItem *file=style->child(c); if(isMarked(file)) items.append(CJobRunner::Item(file->text(0), style->family(), style->value(), 0!=file->text(0).indexOf(home))); } } return items; } void CFontFileListView::removeFiles() { QTreeWidgetItem *root=invisibleRootItem(); QList removeFonts; for(int t=0; tchildCount(); ++t) { QList removeFiles; QTreeWidgetItem *font=root->child(t); for(int c=0; cchildCount(); ++c) { QTreeWidgetItem *file=font->child(c); if(!Misc::fExists(file->text(0))) removeFiles.append(file); } QList::ConstIterator it(removeFiles.begin()), end(removeFiles.end()); for(; it!=end; ++it) delete (*it); if(0==font->childCount()) removeFonts.append(font); } QList::ConstIterator it(removeFonts.begin()), end(removeFonts.end()); for(; it!=end; ++it) delete (*it); } void CFontFileListView::openViewer() { // Number of fonts user has selected, before we ask if they really want to view them all... static const int constMaxBeforePrompt=10; QList items(selectedItems()); QTreeWidgetItem *item; QSet files; foreach(item, items) if(item->parent()) // Then it is a file, not font name :-) files.insert(item->text(0)); if(files.count() && (files.count()::ConstIterator it(files.begin()), end(files.end()); for(; it!=end; ++it) { QStringList args; args << (*it); QProcess::startDetached(Misc::app(KFI_VIEWER), args); } } } void CFontFileListView::properties() { QList items(selectedItems()); QTreeWidgetItem *item; KFileItemList files; foreach(item, items) if(item->parent()) files.append(KFileItem(KUrl::fromPath(item->text(0)), KMimeType::findByPath(item->text(0))->name(), item->text(COL_LINK).isEmpty() ? S_IFREG : S_IFLNK)); if(files.count()) { KPropertiesDialog dlg(files, this); dlg.exec(); } } void CFontFileListView::mark() { QList items(selectedItems()); QTreeWidgetItem *item; foreach(item, items) if(item->parent()) markItem(item); checkFiles(); } void CFontFileListView::unmark() { QList items(selectedItems()); QTreeWidgetItem *item; foreach(item, items) if(item->parent()) unmarkItem(item); checkFiles(); } void CFontFileListView::selectionChanged() { QList items(selectedItems()); QTreeWidgetItem *item; foreach(item, items) if(!item->parent() && item->isSelected()) item->setSelected(false); } void CFontFileListView::clicked(QTreeWidgetItem *item, int col) { if(item && COL_TRASH==col && item->parent()) { if(isMarked(item)) unmarkItem(item); else markItem(item); checkFiles(); } } void CFontFileListView::contextMenuEvent(QContextMenuEvent *ev) { QTreeWidgetItem *item(itemAt(ev->pos())); if(item && item->parent()) { if(!item->isSelected()) item->setSelected(true); bool haveUnmarked(false), haveMarked(false); QList items(selectedItems()); QTreeWidgetItem *item; foreach(item, items) { if(item->parent() && item->isSelected()) { if(isMarked(item)) haveMarked=true; else haveUnmarked=true; } if(haveUnmarked && haveMarked) break; } itsMarkAct->setEnabled(haveUnmarked); itsUnMarkAct->setEnabled(haveMarked); itsMenu->popup(ev->globalPos()); } } void CFontFileListView::checkFiles() { // Need to check that if we mark a file that is linked to, then we also need // to mark the sym link. QSet marked(getMarkedFiles()); if(marked.count()) { QTreeWidgetItem *root=invisibleRootItem(); for(int t=0; tchildCount(); ++t) { QTreeWidgetItem *font=root->child(t); for(int c=0; cchildCount(); ++c) { QTreeWidgetItem *file=font->child(c); QString link(font->child(c)->text(COL_LINK)); if(!link.isEmpty() && marked.contains(link)) if(!isMarked(file)) markItem(file); } } emit haveDeletions(true); } else emit haveDeletions(false); } } #include "moc_DuplicatesDialog.cpp"