File indexing completed on 2025-01-26 05:08:07
0001 /* 0002 SPDX-FileCopyrightText: 2003-2007 Craig Drummond <craig@kde.org> 0003 SPDX-License-Identifier: GPL-2.0-or-later 0004 */ 0005 0006 #include "DuplicatesDialog.h" 0007 #include "ActionLabel.h" 0008 #include "Fc.h" 0009 #include "FcEngine.h" 0010 #include "FontList.h" 0011 #include <KFileItem> 0012 #include <KFormat> 0013 #include <KIconLoader> 0014 #include <KMessageBox> 0015 #include <KPropertiesDialog> 0016 #include <QApplication> 0017 #include <QContextMenuEvent> 0018 #include <QDialogButtonBox> 0019 #include <QDir> 0020 #include <QFileInfo> 0021 #include <QFileInfoList> 0022 #include <QGridLayout> 0023 #include <QHeaderView> 0024 #include <QLabel> 0025 #include <QMenu> 0026 #include <QMimeDatabase> 0027 #include <QProcess> 0028 #include <QPushButton> 0029 #include <QScreen> 0030 #include <QVBoxLayout> 0031 #include <QWindow> 0032 0033 namespace KFI 0034 { 0035 enum EDialogColumns { 0036 COL_FILE, 0037 COL_TRASH, 0038 COL_SIZE, 0039 COL_DATE, 0040 COL_LINK, 0041 }; 0042 0043 CDuplicatesDialog::CDuplicatesDialog(QWidget *parent, CFontList *fl) 0044 : QDialog(parent) 0045 , m_fontList(fl) 0046 { 0047 setWindowTitle(i18n("Duplicate Fonts")); 0048 m_buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel); 0049 connect(m_buttonBox, &QDialogButtonBox::clicked, this, &CDuplicatesDialog::slotButtonClicked); 0050 QVBoxLayout *mainLayout = new QVBoxLayout; 0051 setLayout(mainLayout); 0052 0053 setModal(true); 0054 0055 QFrame *page = new QFrame(this); 0056 mainLayout->addWidget(page); 0057 mainLayout->addWidget(m_buttonBox); 0058 0059 QGridLayout *layout = new QGridLayout(page); 0060 layout->setContentsMargins(0, 0, 0, 0); 0061 0062 m_label = new QLabel(page); 0063 m_view = new CFontFileListView(page); 0064 m_view->hide(); 0065 layout->addWidget(m_actionLabel = new CActionLabel(this), 0, 0); 0066 layout->addWidget(m_label, 0, 1); 0067 m_label->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); 0068 layout->addWidget(m_view, 1, 0, 1, 2); 0069 m_fontFileList = new CFontFileList(this); 0070 connect(m_fontFileList, SIGNAL(finished()), SLOT(scanFinished())); 0071 connect(m_view, &CFontFileListView::haveDeletions, this, &CDuplicatesDialog::enableButtonOk); 0072 } 0073 0074 int CDuplicatesDialog::exec() 0075 { 0076 m_actionLabel->startAnimation(); 0077 m_label->setText(i18n("Scanning for duplicate fonts. Please wait…")); 0078 m_fontFileList->start(); 0079 return QDialog::exec(); 0080 } 0081 0082 void CDuplicatesDialog::scanFinished() 0083 { 0084 m_actionLabel->stopAnimation(); 0085 0086 if (m_fontFileList->wasTerminated()) { 0087 m_fontFileList->wait(); 0088 reject(); 0089 } else { 0090 CFontFileList::TFontMap duplicates; 0091 0092 m_fontFileList->getDuplicateFonts(duplicates); 0093 0094 if (0 == duplicates.count()) { 0095 m_buttonBox->setStandardButtons(QDialogButtonBox::Close); 0096 m_label->setText(i18n("No duplicate fonts found.")); 0097 } else { 0098 QSize sizeB4(size()); 0099 0100 m_buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Close); 0101 QPushButton *okButton = m_buttonBox->button(QDialogButtonBox::Ok); 0102 okButton->setDefault(true); 0103 okButton->setShortcut(Qt::CTRL | Qt::Key_Return); 0104 okButton->setText(i18n("Delete Marked Files")); 0105 okButton->setEnabled(false); 0106 m_label->setText(i18np("%1 duplicate font found.", "%1 duplicate fonts found.", duplicates.count())); 0107 m_view->show(); 0108 0109 CFontFileList::TFontMap::ConstIterator it(duplicates.begin()), end(duplicates.end()); 0110 QFont boldFont(font()); 0111 0112 boldFont.setBold(true); 0113 0114 for (; it != end; ++it) { 0115 QStringList details; 0116 0117 details << FC::createName(it.key().family, it.key().styleInfo); 0118 0119 CFontFileListView::StyleItem *top = new CFontFileListView::StyleItem(m_view, details, it.key().family, it.key().styleInfo); 0120 0121 QSet<QString>::ConstIterator fit((*it).begin()), fend((*it).end()); 0122 int tt(0), t1(0); 0123 0124 for (; fit != fend; ++fit) { 0125 QFileInfo info(*fit); 0126 details.clear(); 0127 details.append(*fit); 0128 details.append(""); 0129 details.append(KFormat().formatByteSize(info.size())); 0130 details.append(QLocale().toString(info.birthTime())); 0131 if (info.isSymLink()) { 0132 details.append(info.symLinkTarget()); 0133 } 0134 new QTreeWidgetItem(top, details); 0135 if (Misc::checkExt(*fit, "pfa") || Misc::checkExt(*fit, "pfb")) { 0136 t1++; 0137 } else { 0138 tt++; 0139 } 0140 } 0141 top->setData(COL_FILE, Qt::DecorationRole, QIcon::fromTheme(t1 > tt ? "application-x-font-type1" : "application-x-font-ttf")); 0142 top->setFont(COL_FILE, boldFont); 0143 } 0144 0145 QTreeWidgetItem *item = nullptr; 0146 for (int i = 0; (item = m_view->topLevelItem(i)); ++i) { 0147 item->setExpanded(true); 0148 } 0149 0150 m_view->setSortingEnabled(true); 0151 m_view->header()->resizeSections(QHeaderView::ResizeToContents); 0152 0153 int width = (m_view->frameWidth() + 8) * 2 + style()->pixelMetric(QStyle::PM_LayoutLeftMargin) + style()->pixelMetric(QStyle::PM_LayoutRightMargin); 0154 0155 for (int i = 0; i < m_view->header()->count(); ++i) { 0156 width += m_view->header()->sectionSize(i); 0157 } 0158 0159 width = qMin(windowHandle()->screen()->size().width(), width); 0160 resize(width, height()); 0161 QSize sizeNow(size()); 0162 if (sizeNow.width() > sizeB4.width()) { 0163 int xmod = (sizeNow.width() - sizeB4.width()) / 2, ymod = (sizeNow.height() - sizeB4.height()) / 2; 0164 0165 move(pos().x() - xmod, pos().y() - ymod); 0166 } 0167 } 0168 } 0169 } 0170 0171 enum EStatus { 0172 STATUS_NO_FILES, 0173 STATUS_ALL_REMOVED, 0174 STATUS_ERROR, 0175 STATUS_USER_CANCELLED, 0176 }; 0177 0178 void CDuplicatesDialog::slotButtonClicked(QAbstractButton *button) 0179 { 0180 switch (m_buttonBox->standardButton(button)) { 0181 case QDialogButtonBox::Ok: { 0182 QSet<QString> files = m_view->getMarkedFiles(); 0183 int fCount = files.count(); 0184 0185 if (1 == fCount ? KMessageBox::PrimaryAction 0186 == KMessageBox::warningTwoActions(this, 0187 i18n("Are you sure you wish to delete:\n%1", *files.begin()), 0188 QString(), 0189 KStandardGuiItem::del(), 0190 KStandardGuiItem::cancel()) 0191 : KMessageBox::PrimaryAction 0192 == KMessageBox::warningTwoActionsList(this, 0193 i18n("Are you sure you wish to delete:"), 0194 files.values(), 0195 QString(), 0196 KStandardGuiItem::del(), 0197 KStandardGuiItem::cancel())) { 0198 m_fontList->setSlowUpdates(true); 0199 0200 CJobRunner runner(this); 0201 0202 connect(&runner, &CJobRunner::configuring, m_fontList, &CFontList::unsetSlowUpdates); 0203 runner.exec(CJobRunner::CMD_REMOVE_FILE, m_view->getMarkedItems(), false); 0204 m_fontList->setSlowUpdates(false); 0205 m_view->removeFiles(); 0206 files = m_view->getMarkedFiles(); 0207 if (fCount != files.count()) { 0208 CFcEngine::setDirty(); 0209 } 0210 if (0 == files.count()) { 0211 accept(); 0212 } 0213 } 0214 break; 0215 } 0216 case QDialogButtonBox::Cancel: 0217 case QDialogButtonBox::Close: 0218 if (!m_fontFileList->wasTerminated()) { 0219 if (m_fontFileList->isRunning()) { 0220 if (KMessageBox::PrimaryAction 0221 == KMessageBox::warningTwoActions(this, i18n("Cancel font scan?"), QString(), KStandardGuiItem::cancel(), KStandardGuiItem::cont())) { 0222 m_label->setText(i18n("Canceling…")); 0223 0224 if (m_fontFileList->isRunning()) { 0225 m_fontFileList->terminate(); 0226 } else { 0227 reject(); 0228 } 0229 } 0230 } else { 0231 reject(); 0232 } 0233 } 0234 break; 0235 default: 0236 break; 0237 } 0238 } 0239 0240 void CDuplicatesDialog::enableButtonOk(bool on) 0241 { 0242 QPushButton *okButton = m_buttonBox->button(QDialogButtonBox::Ok); 0243 if (okButton) { 0244 okButton->setEnabled(on); 0245 } 0246 } 0247 0248 static uint qHash(const CFontFileList::TFile &key) 0249 { 0250 return qHash(key.name.toLower()); 0251 } 0252 0253 CFontFileList::CFontFileList(CDuplicatesDialog *parent) 0254 : QThread(parent) 0255 , m_terminated(false) 0256 { 0257 } 0258 0259 void CFontFileList::start() 0260 { 0261 if (!isRunning()) { 0262 m_terminated = false; 0263 QThread::start(); 0264 } 0265 } 0266 0267 void CFontFileList::terminate() 0268 { 0269 m_terminated = true; 0270 } 0271 0272 void CFontFileList::getDuplicateFonts(TFontMap &map) 0273 { 0274 map = m_map; 0275 0276 if (!map.isEmpty()) { 0277 TFontMap::Iterator it(map.begin()), end(map.end()); 0278 0279 // Now re-iterate, and remove any entries that only have 1 file... 0280 for (it = map.begin(); it != end;) { 0281 if ((*it).count() < 2) { 0282 it = map.erase(it); 0283 } else { 0284 ++it; 0285 } 0286 } 0287 } 0288 } 0289 0290 void CFontFileList::run() 0291 { 0292 const QList<CFamilyItem *> &families(((CDuplicatesDialog *)parent())->fontList()->families()); 0293 QList<CFamilyItem *>::ConstIterator it(families.begin()), end(families.end()); 0294 0295 for (; it != end; ++it) { 0296 QList<CFontItem *>::ConstIterator fontIt((*it)->fonts().begin()), fontEnd((*it)->fonts().end()); 0297 0298 for (; fontIt != fontEnd; ++fontIt) { 0299 if (!(*fontIt)->isBitmap()) { 0300 Misc::TFont font((*fontIt)->family(), (*fontIt)->styleInfo()); 0301 FileCont::ConstIterator fileIt((*fontIt)->files().begin()), fileEnd((*fontIt)->files().end()); 0302 0303 for (; fileIt != fileEnd; ++fileIt) { 0304 if (!Misc::isMetrics((*fileIt).path()) && !Misc::isBitmap((*fileIt).path())) { 0305 m_map[font].insert((*fileIt).path()); 0306 } 0307 } 0308 } 0309 } 0310 } 0311 0312 // if we have 2 fonts: /wibble/a.ttf and /wibble/a.TTF fontconfig only returns the 1st, so we 0313 // now iterate over fontconfig's list, and look for other matching fonts... 0314 if (!m_map.isEmpty() && !m_terminated) { 0315 // Create a map of folder -> set<files> 0316 TFontMap::Iterator it(m_map.begin()), end(m_map.end()); 0317 QHash<QString, QSet<TFile>> folderMap; 0318 0319 for (int n = 0; it != end && !m_terminated; ++it) { 0320 QStringList add; 0321 QSet<QString>::const_iterator fIt((*it).begin()), fEnd((*it).end()); 0322 0323 for (; fIt != fEnd && !m_terminated; ++fIt, ++n) { 0324 folderMap[Misc::getDir(*fIt)].insert(TFile(Misc::getFile(*fIt), it)); 0325 } 0326 } 0327 0328 // Go through our folder map, and check for file duplicates... 0329 QHash<QString, QSet<TFile>>::Iterator folderIt(folderMap.begin()), folderEnd(folderMap.end()); 0330 0331 for (; folderIt != folderEnd && !m_terminated; ++folderIt) { 0332 fileDuplicates(folderIt.key(), *folderIt); 0333 } 0334 } 0335 0336 Q_EMIT finished(); 0337 } 0338 0339 void CFontFileList::fileDuplicates(const QString &folder, const QSet<TFile> &files) 0340 { 0341 QDir dir(folder); 0342 0343 dir.setFilter(QDir::Files | QDir::Hidden); 0344 0345 QFileInfoList list(dir.entryInfoList()); 0346 0347 for (int i = 0; i < list.size() && !m_terminated; ++i) { 0348 QFileInfo fileInfo(list.at(i)); 0349 0350 // Check if this file is already know about - this will do a case-sensitive comparison 0351 if (!files.contains(TFile(fileInfo.fileName()))) { 0352 // OK, not found - this means it is a duplicate, but different case. So, find the 0353 // FontMap iterator, and update its list of files. 0354 QSet<TFile>::ConstIterator entry = files.find(TFile(fileInfo.fileName(), true)); 0355 0356 if (entry != files.end()) { 0357 (*((*entry).it)).insert(fileInfo.absoluteFilePath()); 0358 } 0359 } 0360 } 0361 } 0362 0363 inline void markItem(QTreeWidgetItem *item) 0364 { 0365 item->setData(COL_TRASH, Qt::DecorationRole, QIcon::fromTheme("list-remove")); 0366 } 0367 0368 inline void unmarkItem(QTreeWidgetItem *item) 0369 { 0370 item->setData(COL_TRASH, Qt::DecorationRole, QVariant()); 0371 } 0372 0373 inline bool isMarked(QTreeWidgetItem *item) 0374 { 0375 return item->data(COL_TRASH, Qt::DecorationRole).isValid(); 0376 } 0377 0378 CFontFileListView::CFontFileListView(QWidget *parent) 0379 : QTreeWidget(parent) 0380 { 0381 QStringList headers; 0382 headers.append(i18n("Font/File")); 0383 headers.append(""); 0384 headers.append(i18n("Size")); 0385 headers.append(i18n("Date")); 0386 headers.append(i18n("Links To")); 0387 setHeaderLabels(headers); 0388 headerItem()->setData(COL_TRASH, Qt::DecorationRole, QIcon::fromTheme("user-trash")); 0389 setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::MinimumExpanding); 0390 setSelectionMode(ExtendedSelection); 0391 sortByColumn(COL_FILE, Qt::AscendingOrder); 0392 setSelectionBehavior(SelectRows); 0393 setSortingEnabled(true); 0394 setAllColumnsShowFocus(true); 0395 setAlternatingRowColors(true); 0396 0397 m_menu = new QMenu(this); 0398 if (!Misc::app(KFI_VIEWER).isEmpty()) { 0399 m_menu->addAction(QIcon::fromTheme("kfontview"), i18n("Open in Font Viewer"), this, &CFontFileListView::openViewer); 0400 } 0401 m_menu->addAction(QIcon::fromTheme("document-properties"), i18n("Properties"), this, &CFontFileListView::properties); 0402 m_menu->addSeparator(); 0403 m_unMarkAct = m_menu->addAction(i18n("Unmark for Deletion"), this, &CFontFileListView::unmark); 0404 m_markAct = m_menu->addAction(QIcon::fromTheme("edit-delete"), i18n("Mark for Deletion"), this, &CFontFileListView::mark); 0405 0406 connect(this, SIGNAL(itemSelectionChanged()), SLOT(selectionChanged())); 0407 connect(this, SIGNAL(itemClicked(QTreeWidgetItem *, int)), SLOT(clicked(QTreeWidgetItem *, int))); 0408 } 0409 0410 QSet<QString> CFontFileListView::getMarkedFiles() 0411 { 0412 QTreeWidgetItem *root = invisibleRootItem(); 0413 QSet<QString> files; 0414 0415 for (int t = 0; t < root->childCount(); ++t) { 0416 QTreeWidgetItem *font = root->child(t); 0417 0418 for (int c = 0; c < font->childCount(); ++c) { 0419 QTreeWidgetItem *file = font->child(c); 0420 0421 if (isMarked(file)) { 0422 files.insert(file->text(0)); 0423 } 0424 } 0425 } 0426 0427 return files; 0428 } 0429 0430 CJobRunner::ItemList CFontFileListView::getMarkedItems() 0431 { 0432 QTreeWidgetItem *root = invisibleRootItem(); 0433 CJobRunner::ItemList items; 0434 QString home(Misc::dirSyntax(QDir::homePath())); 0435 0436 for (int t = 0; t < root->childCount(); ++t) { 0437 StyleItem *style = (StyleItem *)root->child(t); 0438 0439 for (int c = 0; c < style->childCount(); ++c) { 0440 QTreeWidgetItem *file = style->child(c); 0441 0442 if (isMarked(file)) { 0443 items.append(CJobRunner::Item(file->text(0), style->family(), style->value(), 0 != file->text(0).indexOf(home))); 0444 } 0445 } 0446 } 0447 0448 return items; 0449 } 0450 0451 void CFontFileListView::removeFiles() 0452 { 0453 QTreeWidgetItem *root = invisibleRootItem(); 0454 QList<QTreeWidgetItem *> removeFonts; 0455 0456 for (int t = 0; t < root->childCount(); ++t) { 0457 QList<QTreeWidgetItem *> removeFiles; 0458 QTreeWidgetItem *font = root->child(t); 0459 0460 for (int c = 0; c < font->childCount(); ++c) { 0461 QTreeWidgetItem *file = font->child(c); 0462 0463 if (!Misc::fExists(file->text(0))) { 0464 removeFiles.append(file); 0465 } 0466 } 0467 0468 QList<QTreeWidgetItem *>::ConstIterator it(removeFiles.begin()), end(removeFiles.end()); 0469 0470 for (; it != end; ++it) { 0471 delete (*it); 0472 } 0473 if (0 == font->childCount()) { 0474 removeFonts.append(font); 0475 } 0476 } 0477 0478 QList<QTreeWidgetItem *>::ConstIterator it(removeFonts.begin()), end(removeFonts.end()); 0479 for (; it != end; ++it) { 0480 delete (*it); 0481 } 0482 } 0483 0484 void CFontFileListView::openViewer() 0485 { 0486 // Number of fonts user has selected, before we ask if they really want to view them all... 0487 static const int constMaxBeforePrompt = 10; 0488 0489 const QList<QTreeWidgetItem *> items(selectedItems()); 0490 QSet<QString> files; 0491 0492 for (QTreeWidgetItem *const item : items) { 0493 if (item->parent()) { // Then it is a file, not font name :-) 0494 files.insert(item->text(0)); 0495 } 0496 } 0497 0498 if (!files.isEmpty() 0499 && (files.count() < constMaxBeforePrompt 0500 || KMessageBox::PrimaryAction 0501 == KMessageBox::questionTwoActions(this, 0502 i18np("Open font in font viewer?", "Open all %1 fonts in font viewer?", files.count()), 0503 QString(), 0504 KStandardGuiItem::open(), 0505 KStandardGuiItem::cancel()))) { 0506 QSet<QString>::ConstIterator it(files.begin()), end(files.end()); 0507 0508 for (; it != end; ++it) { 0509 QStringList args; 0510 0511 args << (*it); 0512 0513 QProcess::startDetached(Misc::app(KFI_VIEWER), args); 0514 } 0515 } 0516 } 0517 0518 void CFontFileListView::properties() 0519 { 0520 const QList<QTreeWidgetItem *> items(selectedItems()); 0521 KFileItemList files; 0522 QMimeDatabase db; 0523 0524 for (QTreeWidgetItem *const item : items) { 0525 if (item->parent()) { 0526 files.append( 0527 KFileItem(QUrl::fromLocalFile(item->text(0)), db.mimeTypeForFile(item->text(0)).name(), item->text(COL_LINK).isEmpty() ? S_IFREG : S_IFLNK)); 0528 } 0529 } 0530 0531 if (!files.isEmpty()) { 0532 KPropertiesDialog dlg(files, this); 0533 dlg.exec(); 0534 } 0535 } 0536 0537 void CFontFileListView::mark() 0538 { 0539 const QList<QTreeWidgetItem *> items(selectedItems()); 0540 0541 for (QTreeWidgetItem *const item : items) { 0542 if (item->parent()) { 0543 markItem(item); 0544 } 0545 } 0546 checkFiles(); 0547 } 0548 0549 void CFontFileListView::unmark() 0550 { 0551 const QList<QTreeWidgetItem *> items(selectedItems()); 0552 0553 for (QTreeWidgetItem *const item : items) { 0554 if (item->parent()) { 0555 unmarkItem(item); 0556 } 0557 } 0558 checkFiles(); 0559 } 0560 0561 void CFontFileListView::selectionChanged() 0562 { 0563 const QList<QTreeWidgetItem *> items(selectedItems()); 0564 0565 for (QTreeWidgetItem *const item : items) { 0566 if (!item->parent() && item->isSelected()) { 0567 item->setSelected(false); 0568 } 0569 } 0570 } 0571 0572 void CFontFileListView::clicked(QTreeWidgetItem *item, int col) 0573 { 0574 if (item && COL_TRASH == col && item->parent()) { 0575 if (isMarked(item)) { 0576 unmarkItem(item); 0577 } else { 0578 markItem(item); 0579 } 0580 checkFiles(); 0581 } 0582 } 0583 0584 void CFontFileListView::contextMenuEvent(QContextMenuEvent *ev) 0585 { 0586 QTreeWidgetItem *item(itemAt(ev->pos())); 0587 0588 if (item && item->parent()) { 0589 if (!item->isSelected()) { 0590 item->setSelected(true); 0591 } 0592 0593 bool haveUnmarked(false), haveMarked(false); 0594 0595 const QList<QTreeWidgetItem *> items(selectedItems()); 0596 0597 for (QTreeWidgetItem *const item : items) { 0598 if (item->parent() && item->isSelected()) { 0599 if (isMarked(item)) { 0600 haveMarked = true; 0601 } else { 0602 haveUnmarked = true; 0603 } 0604 } 0605 0606 if (haveUnmarked && haveMarked) { 0607 break; 0608 } 0609 } 0610 0611 m_markAct->setEnabled(haveUnmarked); 0612 m_unMarkAct->setEnabled(haveMarked); 0613 m_menu->popup(ev->globalPos()); 0614 } 0615 } 0616 0617 void CFontFileListView::checkFiles() 0618 { 0619 // Need to check that if we mark a file that is linked to, then we also need 0620 // to mark the sym link. 0621 QSet<QString> marked(getMarkedFiles()); 0622 0623 if (marked.count()) { 0624 QTreeWidgetItem *root = invisibleRootItem(); 0625 0626 for (int t = 0; t < root->childCount(); ++t) { 0627 QTreeWidgetItem *font = root->child(t); 0628 0629 for (int c = 0; c < font->childCount(); ++c) { 0630 QTreeWidgetItem *file = font->child(c); 0631 QString link(font->child(c)->text(COL_LINK)); 0632 0633 if (!link.isEmpty() && marked.contains(link)) { 0634 if (!isMarked(file)) { 0635 markItem(file); 0636 } 0637 } 0638 } 0639 } 0640 0641 Q_EMIT haveDeletions(true); 0642 } else { 0643 Q_EMIT haveDeletions(false); 0644 } 0645 } 0646 }