#include "ui/AnalyzeDialog.h"
#include <algorithm>
#include <QFrame>
#include <QGridLayout>
#include <QHash>
#include <QHBoxLayout>
#include <QLabel>
#include <QList>
#include <QLocale>
#include <QPair>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QWidget>
namespace whittle {
namespace {
constexpr char kHeadStyle[] = "color: #999999; font-size: 8pt;";
constexpr char kRuleStyle[] = "color: #cccccc; background-color: #cccccc;";
constexpr char kEmptyStyle[] = "color: #999999;";
constexpr char kSheet[] =
"QDialog { background: #F4F4F4; }"
"QScrollArea#area { border: 1px solid #c1c1c1; border-radius: 4px;"
" background: #FAFAFA; }"
"QWidget#body { background: transparent; }"
"QLabel { color: #222222; }";
constexpr int kHeadGap = 4;
constexpr int kSectionGap = 12;
constexpr int kRuleGap = 8;
constexpr int kRowSpacing = 3;
constexpr int kLabelGap = 15;
constexpr int kBodyPad = 10;
constexpr int kMaxExt = 10;
enum Cat { Images, Video, Audio, Documents, Archives, Code, System, Other,
CatCount };
struct TypeInfo { const char* ext; const char* name; Cat cat; };
constexpr TypeInfo kTypes[] = {
{"jpg", "JPEG Image", Images}, {"jpeg", "JPEG Image", Images},
{"png", "PNG Image", Images}, {"gif", "GIF Image", Images},
{"bmp", "Bitmap Image", Images}, {"tif", "TIFF Image", Images},
{"tiff", "TIFF Image", Images}, {"webp", "WebP Image", Images},
{"heic", "HEIC Image", Images}, {"avif", "AVIF Image", Images},
{"svg", "SVG Image", Images}, {"ico", "Icon", Images},
{"psd", "Photoshop Image", Images}, {"xcf", "GIMP Image", Images},
{"raw", "Raw Image", Images}, {"cr2", "Canon Raw", Images},
{"nef", "Nikon Raw", Images}, {"dng", "DNG Raw", Images},
{"mp4", "MPEG-4 Video", Video}, {"m4v", "MPEG-4 Video", Video},
{"mkv", "Matroska Video", Video}, {"webm", "WebM Video", Video},
{"avi", "AVI Video", Video}, {"mov", "QuickTime Video", Video},
{"wmv", "Windows Media Video", Video}, {"flv", "Flash Video", Video},
{"mpg", "MPEG Video", Video}, {"mpeg", "MPEG Video", Video},
{"3gp", "3GP Video", Video},
{"mp3", "MP3 Audio", Audio}, {"flac", "FLAC Audio", Audio},
{"wav", "WAV Audio", Audio}, {"ogg", "Ogg Audio", Audio},
{"opus", "Opus Audio", Audio}, {"m4a", "AAC Audio", Audio},
{"aac", "AAC Audio", Audio}, {"wma", "Windows Media Audio", Audio},
{"aiff", "AIFF Audio", Audio}, {"mid", "MIDI", Audio},
{"pdf", "PDF Document", Documents}, {"txt", "Text", Documents},
{"md", "Markdown", Documents}, {"rtf", "Rich Text", Documents},
{"doc", "Word Document", Documents}, {"docx", "Word Document", Documents},
{"odt", "OpenDocument Text", Documents},
{"xls", "Excel Spreadsheet", Documents},
{"xlsx", "Excel Spreadsheet", Documents},
{"ods", "OpenDocument Sheet", Documents},
{"csv", "CSV Data", Documents}, {"ppt", "PowerPoint", Documents},
{"pptx", "PowerPoint", Documents}, {"odp", "OpenDocument Slides", Documents},
{"epub", "EPUB Book", Documents}, {"html", "HTML Document", Documents},
{"htm", "HTML Document", Documents},
{"zip", "ZIP Archive", Archives}, {"7z", "7-Zip Archive", Archives},
{"rar", "RAR Archive", Archives}, {"tar", "Tar Archive", Archives},
{"gz", "Gzip Archive", Archives}, {"tgz", "Tar Gzip Archive", Archives},
{"bz2", "Bzip2 Archive", Archives}, {"xz", "XZ Archive", Archives},
{"zst", "Zstd Archive", Archives}, {"iso", "Disc Image", Archives},
{"img", "Disk Image", Archives}, {"deb", "Debian Package", Archives},
{"rpm", "RPM Package", Archives}, {"apk", "Android Package", Archives},
{"appimage", "AppImage", Archives},
{"c", "C Source", Code}, {"h", "C Header", Code},
{"cpp", "C++ Source", Code}, {"cc", "C++ Source", Code},
{"hpp", "C++ Header", Code}, {"py", "Python", Code},
{"js", "JavaScript", Code}, {"ts", "TypeScript", Code},
{"java", "Java Source", Code}, {"kt", "Kotlin Source", Code},
{"rs", "Rust Source", Code}, {"go", "Go Source", Code},
{"sh", "Shell Script", Code}, {"json", "JSON", Code},
{"xml", "XML", Code}, {"yaml", "YAML", Code},
{"yml", "YAML", Code}, {"toml", "TOML", Code},
{"css", "CSS Stylesheet", Code}, {"qml", "QML", Code},
{"gradle", "Gradle Script", Code}, {"cmake", "CMake Script", Code},
{"pro", "Qt Project", Code}, {"ui", "Qt Designer Form", Code},
{"qrc", "Qt Resource", Code},
{"so", "Shared Library", System}, {"a", "Static Library", System},
{"o", "Object File", System}, {"ko", "Kernel Module", System},
{"exe", "Windows Executable", System}, {"dll", "Windows Library", System},
{"msi", "Windows Installer", System}, {"bin", "Binary", System},
{"dat", "Data", System}, {"db", "Database", System},
{"sqlite", "SQLite Database", System}, {"conf", "Config", System},
{"cfg", "Config", System}, {"ini", "Config", System},
{"desktop", "Desktop Entry", System}, {"service", "Systemd Unit", System},
{"log", "Log", System}, {"tmp", "Temporary", System},
{"bak", "Backup", System}, {"lock", "Lock File", System},
{"cache", "Cache", System}, {"sys", "System File", System},
{"lnk", "Windows Shortcut", System},
};
using Tally = QList<QPair<QString, int>>;
const TypeInfo* lookup(const QString& ext) {
static const QHash<QString, const TypeInfo*> map = [] {
QHash<QString, const TypeInfo*> m;
for (const TypeInfo& t : kTypes) m.insert(QLatin1String(t.ext), &t);
return m;
}();
return map.value(ext, nullptr);
}
QString extOf(const QString& relPath) {
const QString name = relPath.section('/', -1);
const int dot = name.lastIndexOf('.');
if (dot <= 0 || dot == name.size() - 1) return {};
const QString ext = name.mid(dot + 1).toLower();
if (ext.size() > kMaxExt) return {};
for (QChar c : ext)
if (!c.isLetterOrNumber() && c != '_' && c != '-') return {};
return ext;
}
Cat catOf(const QString& ext) {
const TypeInfo* t = ext.isEmpty() ? nullptr : lookup(ext);
return t ? t->cat : Other;
}
QString typeLabel(const QString& ext) {
if (ext.isEmpty()) return AnalyzeDialog::tr("No Extension");
const TypeInfo* t = lookup(ext);
const QString name = t ? QString::fromLatin1(t->name)
: ext.toUpper() + AnalyzeDialog::tr(" File");
return QStringLiteral("%1 (%2)").arg(name, ext);
}
QHash<QString, int> countExt(const QStringList& files) {
QHash<QString, int> n;
for (const QString& f : files) ++n[extOf(f)];
return n;
}
QFrame* divider() {
auto* hr = new QFrame;
hr->setFrameShape(QFrame::HLine);
hr->setFrameShadow(QFrame::Plain);
hr->setStyleSheet(QLatin1String(kRuleStyle));
hr->setFixedHeight(1);
hr->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
return hr;
}
QWidget* sectionHeader(const QString& title) {
auto* w = new QWidget;
auto* h = new QHBoxLayout(w);
h->setContentsMargins(0, 0, 0, 0);
h->setSpacing(8);
auto* label = new QLabel(title);
label->setStyleSheet(QLatin1String(kHeadStyle));
label->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Fixed);
h->addWidget(label);
h->addWidget(divider(), 1);
return w;
}
void addSection(QVBoxLayout* v, const QString& title, Tally rows) {
std::sort(rows.begin(), rows.end(), [](const auto& a, const auto& b) {
return a.second != b.second ? a.second > b.second : a.first < b.first;
});
if (!title.isEmpty()) {
if (v->count() > 0) v->addSpacing(kSectionGap);
v->addWidget(sectionHeader(title));
v->addSpacing(kHeadGap);
} else if (v->count() > 0) {
v->addSpacing(kRuleGap);
v->addWidget(divider());
v->addSpacing(kRuleGap);
}
auto* grid = new QGridLayout;
grid->setContentsMargins(0, 0, 0, 0);
grid->setHorizontalSpacing(kLabelGap);
grid->setVerticalSpacing(kRowSpacing);
grid->setColumnStretch(0, 1);
const QLocale loc;
for (int i = 0; i < rows.size(); ++i) {
auto* name = new QLabel(typeLabel(rows[i].first));
auto* count = new QLabel(loc.toString(rows[i].second));
count->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
grid->addWidget(name, i, 0);
grid->addWidget(count, i, 1);
}
v->addLayout(grid);
}
Tally toTally(const QHash<QString, int>& counts) {
Tally t;
for (auto it = counts.constBegin(); it != counts.constEnd(); ++it)
t.append({it.key(), it.value()});
return t;
}
}
AnalyzeDialog::AnalyzeDialog(const QStringList& files,
const QStringList& deleted, bool merged,
QWidget* parent)
: QDialog(parent) {
setWindowTitle(merged ? tr("Analyze: Merge") : tr("Analyze: Scan"));
setModal(true);
resize(360, 460);
setStyleSheet(QLatin1String(kSheet));
auto* body = new QWidget;
body->setObjectName(QStringLiteral("body"));
auto* bv = new QVBoxLayout(body);
bv->setContentsMargins(kBodyPad, kBodyPad, kBodyPad, kBodyPad);
bv->setSpacing(0);
Tally byCat[CatCount];
const QHash<QString, int> counts = countExt(files);
for (auto it = counts.constBegin(); it != counts.constEnd(); ++it)
byCat[catOf(it.key())].append({it.key(), it.value()});
for (int c = 0; c < CatCount; ++c)
if (!byCat[c].isEmpty()) addSection(bv, QString(), byCat[c]);
if (!deleted.isEmpty())
addSection(bv, tr("Deleted"), toTally(countExt(deleted)));
if (bv->count() == 0) {
auto* none = new QLabel(tr("No files."));
none->setStyleSheet(QLatin1String(kEmptyStyle));
bv->addWidget(none);
}
bv->addStretch(1);
auto* area = new QScrollArea;
area->setObjectName(QStringLiteral("area"));
area->setWidgetResizable(true);
area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
area->setWidget(body);
area->viewport()->setAutoFillBackground(false);
auto* v = new QVBoxLayout(this);
v->setContentsMargins(16, 14, 16, 14);
v->setSpacing(0);
v->addWidget(area);
}
}