You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
632 lines
19 KiB
632 lines
19 KiB
/* |
|
SPDX-FileCopyrightText: 2003-2007 Craig Drummond <craig@kde.org> |
|
SPDX-License-Identifier: GPL-2.0-or-later |
|
*/ |
|
|
|
#include "DuplicatesDialog.h" |
|
#include "ActionLabel.h" |
|
#include "Fc.h" |
|
#include "FcEngine.h" |
|
#include "FontList.h" |
|
#include <KFileItem> |
|
#include <KFormat> |
|
#include <KIconLoader> |
|
#include <KMessageBox> |
|
#include <KPropertiesDialog> |
|
#include <QApplication> |
|
#include <QContextMenuEvent> |
|
#include <QDesktopWidget> |
|
#include <QDialogButtonBox> |
|
#include <QDir> |
|
#include <QFileInfo> |
|
#include <QFileInfoList> |
|
#include <QGridLayout> |
|
#include <QHeaderView> |
|
#include <QLabel> |
|
#include <QMenu> |
|
#include <QMimeDatabase> |
|
#include <QProcess> |
|
#include <QPushButton> |
|
#include <QVBoxLayout> |
|
|
|
namespace KFI |
|
{ |
|
enum EDialogColumns { |
|
COL_FILE, |
|
COL_TRASH, |
|
COL_SIZE, |
|
COL_DATE, |
|
COL_LINK, |
|
}; |
|
|
|
CDuplicatesDialog::CDuplicatesDialog(QWidget *parent, CFontList *fl) |
|
: QDialog(parent) |
|
, itsFontList(fl) |
|
{ |
|
setWindowTitle(i18n("Duplicate Fonts")); |
|
itsButtonBox = new QDialogButtonBox(QDialogButtonBox::Cancel); |
|
connect(itsButtonBox, &QDialogButtonBox::clicked, this, &CDuplicatesDialog::slotButtonClicked); |
|
QVBoxLayout *mainLayout = new QVBoxLayout; |
|
setLayout(mainLayout); |
|
|
|
setModal(true); |
|
|
|
QFrame *page = new QFrame(this); |
|
mainLayout->addWidget(page); |
|
mainLayout->addWidget(itsButtonBox); |
|
|
|
QGridLayout *layout = new QGridLayout(page); |
|
layout->setContentsMargins(0, 0, 0, 0); |
|
|
|
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, &CFontFileListView::haveDeletions, this, &CDuplicatesDialog::enableButtonOk); |
|
} |
|
|
|
int CDuplicatesDialog::exec() |
|
{ |
|
itsActionLabel->startAnimation(); |
|
itsLabel->setText(i18n("Scanning for duplicate fonts. Please wait…")); |
|
itsFontFileList->start(); |
|
return QDialog::exec(); |
|
} |
|
|
|
void CDuplicatesDialog::scanFinished() |
|
{ |
|
itsActionLabel->stopAnimation(); |
|
|
|
if (itsFontFileList->wasTerminated()) { |
|
itsFontFileList->wait(); |
|
reject(); |
|
} else { |
|
CFontFileList::TFontMap duplicates; |
|
|
|
itsFontFileList->getDuplicateFonts(duplicates); |
|
|
|
if (0 == duplicates.count()) { |
|
itsButtonBox->setStandardButtons(QDialogButtonBox::Close); |
|
itsLabel->setText(i18n("No duplicate fonts found.")); |
|
} else { |
|
QSize sizeB4(size()); |
|
|
|
itsButtonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Close); |
|
QPushButton *okButton = itsButtonBox->button(QDialogButtonBox::Ok); |
|
okButton->setDefault(true); |
|
okButton->setShortcut(Qt::CTRL | Qt::Key_Return); |
|
okButton->setText(i18n("Delete Marked Files")); |
|
okButton->setEnabled(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<QString>::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(KFormat().formatByteSize(info.size())); |
|
details.append(QLocale().toString(info.birthTime())); |
|
if (info.isSymLink()) { |
|
details.append(info.symLinkTarget()); |
|
} |
|
new QTreeWidgetItem(top, details); |
|
if (Misc::checkExt(*fit, "pfa") || Misc::checkExt(*fit, "pfb")) { |
|
t1++; |
|
} else { |
|
tt++; |
|
} |
|
} |
|
top->setData(COL_FILE, Qt::DecorationRole, QIcon::fromTheme(t1 > tt ? "application-x-font-type1" : "application-x-font-ttf")); |
|
top->setFont(COL_FILE, boldFont); |
|
} |
|
|
|
QTreeWidgetItem *item = nullptr; |
|
for (int i = 0; (item = itsView->topLevelItem(i)); ++i) { |
|
item->setExpanded(true); |
|
} |
|
|
|
itsView->setSortingEnabled(true); |
|
itsView->header()->resizeSections(QHeaderView::ResizeToContents); |
|
|
|
int width = |
|
(itsView->frameWidth() + 8) * 2 + style()->pixelMetric(QStyle::PM_LayoutLeftMargin) + style()->pixelMetric(QStyle::PM_LayoutRightMargin); |
|
|
|
for (int i = 0; i < itsView->header()->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(QAbstractButton *button) |
|
{ |
|
switch (itsButtonBox->standardButton(button)) { |
|
case QDialogButtonBox::Ok: { |
|
QSet<QString> 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.begin())) |
|
: KMessageBox::Yes == KMessageBox::warningYesNoList(this, i18n("Are you sure you wish to delete:"), files.values())) { |
|
itsFontList->setSlowUpdates(true); |
|
|
|
CJobRunner runner(this); |
|
|
|
connect(&runner, &CJobRunner::configuring, itsFontList, &CFontList::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 QDialogButtonBox::Cancel: |
|
case QDialogButtonBox::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; |
|
} |
|
} |
|
|
|
void CDuplicatesDialog::enableButtonOk(bool on) |
|
{ |
|
QPushButton *okButton = itsButtonBox->button(QDialogButtonBox::Ok); |
|
if (okButton) { |
|
okButton->setEnabled(on); |
|
} |
|
} |
|
|
|
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.isEmpty()) { |
|
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<CFamilyItem *> &families(((CDuplicatesDialog *)parent())->fontList()->families()); |
|
QList<CFamilyItem *>::ConstIterator it(families.begin()), end(families.end()); |
|
|
|
for (; it != end; ++it) { |
|
QList<CFontItem *>::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.isEmpty() && !itsTerminated) { |
|
// Create a map of folder -> set<files> |
|
TFontMap::Iterator it(itsMap.begin()), end(itsMap.end()); |
|
QHash<QString, QSet<TFile>> folderMap; |
|
|
|
for (int n = 0; it != end && !itsTerminated; ++it) { |
|
QStringList add; |
|
QSet<QString>::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<QString, QSet<TFile>>::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<TFile> &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<TFile>::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, QIcon::fromTheme("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, QIcon::fromTheme("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(QIcon::fromTheme("kfontview"), i18n("Open in Font Viewer"), this, &CFontFileListView::openViewer); |
|
} |
|
itsMenu->addAction(QIcon::fromTheme("document-properties"), i18n("Properties"), this, &CFontFileListView::properties); |
|
itsMenu->addSeparator(); |
|
itsUnMarkAct = itsMenu->addAction(i18n("Unmark for Deletion"), this, &CFontFileListView::unmark); |
|
itsMarkAct = itsMenu->addAction(QIcon::fromTheme("edit-delete"), i18n("Mark for Deletion"), this, &CFontFileListView::mark); |
|
|
|
connect(this, SIGNAL(itemSelectionChanged()), SLOT(selectionChanged())); |
|
connect(this, SIGNAL(itemClicked(QTreeWidgetItem *, int)), SLOT(clicked(QTreeWidgetItem *, int))); |
|
} |
|
|
|
QSet<QString> CFontFileListView::getMarkedFiles() |
|
{ |
|
QTreeWidgetItem *root = invisibleRootItem(); |
|
QSet<QString> files; |
|
|
|
for (int t = 0; t < root->childCount(); ++t) { |
|
QList<QTreeWidgetItem *> removeFiles; |
|
QTreeWidgetItem *font = root->child(t); |
|
|
|
for (int c = 0; c < font->childCount(); ++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; t < root->childCount(); ++t) { |
|
QList<QTreeWidgetItem *> removeFiles; |
|
StyleItem *style = (StyleItem *)root->child(t); |
|
|
|
for (int c = 0; c < style->childCount(); ++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<QTreeWidgetItem *> removeFonts; |
|
|
|
for (int t = 0; t < root->childCount(); ++t) { |
|
QList<QTreeWidgetItem *> removeFiles; |
|
QTreeWidgetItem *font = root->child(t); |
|
|
|
for (int c = 0; c < font->childCount(); ++c) { |
|
QTreeWidgetItem *file = font->child(c); |
|
|
|
if (!Misc::fExists(file->text(0))) { |
|
removeFiles.append(file); |
|
} |
|
} |
|
|
|
QList<QTreeWidgetItem *>::ConstIterator it(removeFiles.begin()), end(removeFiles.end()); |
|
|
|
for (; it != end; ++it) { |
|
delete (*it); |
|
} |
|
if (0 == font->childCount()) { |
|
removeFonts.append(font); |
|
} |
|
} |
|
|
|
QList<QTreeWidgetItem *>::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<QTreeWidgetItem *> items(selectedItems()); |
|
QTreeWidgetItem *item; |
|
QSet<QString> files; |
|
|
|
foreach (item, items) |
|
if (item->parent()) { // Then it is a file, not font name :-) |
|
files.insert(item->text(0)); |
|
} |
|
|
|
if (!files.isEmpty() |
|
&& (files.count() < constMaxBeforePrompt |
|
|| KMessageBox::Yes == KMessageBox::questionYesNo(this, i18np("Open font in font viewer?", "Open all %1 fonts in font viewer?", files.count())))) { |
|
QSet<QString>::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<QTreeWidgetItem *> items(selectedItems()); |
|
QTreeWidgetItem *item; |
|
KFileItemList files; |
|
QMimeDatabase db; |
|
|
|
foreach (item, items) |
|
if (item->parent()) { |
|
files.append( |
|
KFileItem(QUrl::fromLocalFile(item->text(0)), db.mimeTypeForFile(item->text(0)).name(), item->text(COL_LINK).isEmpty() ? S_IFREG : S_IFLNK)); |
|
} |
|
|
|
if (!files.isEmpty()) { |
|
KPropertiesDialog dlg(files, this); |
|
dlg.exec(); |
|
} |
|
} |
|
|
|
void CFontFileListView::mark() |
|
{ |
|
QList<QTreeWidgetItem *> items(selectedItems()); |
|
QTreeWidgetItem *item; |
|
|
|
foreach (item, items) |
|
if (item->parent()) { |
|
markItem(item); |
|
} |
|
checkFiles(); |
|
} |
|
|
|
void CFontFileListView::unmark() |
|
{ |
|
QList<QTreeWidgetItem *> items(selectedItems()); |
|
QTreeWidgetItem *item; |
|
|
|
foreach (item, items) |
|
if (item->parent()) { |
|
unmarkItem(item); |
|
} |
|
checkFiles(); |
|
} |
|
|
|
void CFontFileListView::selectionChanged() |
|
{ |
|
QList<QTreeWidgetItem *> 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<QTreeWidgetItem *> 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<QString> marked(getMarkedFiles()); |
|
|
|
if (marked.count()) { |
|
QTreeWidgetItem *root = invisibleRootItem(); |
|
|
|
for (int t = 0; t < root->childCount(); ++t) { |
|
QTreeWidgetItem *font = root->child(t); |
|
|
|
for (int c = 0; c < font->childCount(); ++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); |
|
} |
|
} |
|
}
|
|
|