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 }