From 67be79385f0b22fe6428e213d2b6422742d994c4 Mon Sep 17 00:00:00 2001 From: Chris Xiong Date: Mon, 29 Aug 2022 00:30:57 -0400 Subject: Add mingui. --- mingui/CMakeLists.txt | 24 +++++ mingui/README.md | 7 ++ mingui/main.cpp | 150 +++++++++++++++++++++++++++ mingui/mingui.cpp | 279 ++++++++++++++++++++++++++++++++++++++++++++++++++ mingui/mingui.hpp | 64 ++++++++++++ 5 files changed, 524 insertions(+) create mode 100644 mingui/CMakeLists.txt create mode 100644 mingui/README.md create mode 100644 mingui/main.cpp create mode 100644 mingui/mingui.cpp create mode 100644 mingui/mingui.hpp (limited to 'mingui') diff --git a/mingui/CMakeLists.txt b/mingui/CMakeLists.txt new file mode 100644 index 0000000..3a53455 --- /dev/null +++ b/mingui/CMakeLists.txt @@ -0,0 +1,24 @@ +cmake_minimum_required(VERSION 3.10.0) + +project(mingui VERSION 0.0.1 LANGUAGES CXX) + +set(CMAKE_INCLUDE_CURRENT_DIR ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +find_package(Qt5 REQUIRED COMPONENTS Widgets) + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +add_executable(mingui + main.cpp + mingui.cpp +) + +target_link_libraries(mingui + Qt5::Widgets +) diff --git a/mingui/README.md b/mingui/README.md new file mode 100644 index 0000000..dff674f --- /dev/null +++ b/mingui/README.md @@ -0,0 +1,7 @@ +# Minimal GUI for Deduper + +Very minimalistic, but working, prototype GUI for deduper. + +For testing purposes only. Not part of the main deduper project (yet). + +Beware extremely ugly code. diff --git a/mingui/main.cpp b/mingui/main.cpp new file mode 100644 index 0000000..8274b6f --- /dev/null +++ b/mingui/main.cpp @@ -0,0 +1,150 @@ +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "mingui.hpp" + +using std::size_t; + +std::unordered_map p; +std::vector fns; +std::map, double> dist; +std::vector par; +std::vector> lists; + +MinGuiWidget *w = nullptr; +size_t curlist; + +size_t get_root(size_t x) +{ + if (x != par[x]) + return par[x] = get_root(par[x]); + return x; +} + +void combine(size_t x, size_t y) +{ + x = get_root(x); + y = get_root(y); + par[x] = y; +} + +void load_result(const char* rp) +{ + FILE *f = fopen(rp, "rb"); + while (1) + { + int l; + double d; + std::string s1, s2; + if (feof(f)) break; + fread(&l, sizeof(int), 1, f); + s1.resize(l); + fread(s1.data(), 1, l, f); + p.try_emplace(s1, p.size() + 1); + fread(&l, sizeof(int), 1, f); + s2.resize(l); + fread(s2.data(), 1, l, f); + p.try_emplace(s2, p.size() + 1); + fread(&d, sizeof(double), 1, f); + dist[std::make_pair(p[s1], p[s2])] = d; + } + fclose(f); +} + +std::vector build_list(const std::vector &l) +{ + std::vector ret; + for (auto &x : l) + ret.push_back(fns[x]); + return ret; +} + +std::map, double> build_dists(const std::vector &l) +{ + std::map, double> ret; + for (size_t i = 0; i < l.size(); ++i) + { + for (size_t j = i + 1; j < l.size(); ++j) + { + size_t x = l[i], y = l[j]; + if (dist.find(std::make_pair(x, y)) != dist.end()) + ret[std::make_pair(i, j)] = dist[std::make_pair(x, y)]; + else if (dist.find(std::make_pair(y, x)) != dist.end()) + ret[std::make_pair(i, j)] = dist[std::make_pair(y, x)]; + } + } + return ret; +} + +int main(int argc, char **argv) +{ + if (argc < 2) return 1; + + load_result(argv[1]); + printf("%lu known files\n", p.size()); + + par.resize(p.size() + 1); + fns.resize(p.size() + 1); + lists.resize(p.size() + 1); + for (auto &kp : p) + fns[kp.second] = kp.first; + + for (size_t i = 1; i < par.size(); ++i) + par[i] = i; + for (auto &kp : dist) + { + auto p = kp.first; + combine(p.first, p.second); + } + for (size_t i = 1; i < par.size(); ++i) + lists[get_root(i)].push_back(i); + + auto listend = std::remove_if(lists.begin(), lists.end(), [](auto &a){return a.size() < 2;}); + lists.erase(listend, lists.end()); + if (lists.empty()) return 0; + for (auto &l : lists) + { + if (l.size()) + { + for (auto &x : l) + printf("%s,", fns[x].c_str()); + puts(""); + } + } + fflush(stdout); + + QApplication a(argc, argv); + + curlist = 0; + w = new MinGuiWidget(); + w->show_images(build_list(lists[curlist])); + w->update_distances(build_dists(lists[curlist])); + w->update_permamsg(curlist, lists.size()); + w->show(); + QObject::connect(w, &MinGuiWidget::next, + []{ + if (curlist < lists.size() - 1) ++curlist; + w->show_images(build_list(lists[curlist])); + w->update_distances(build_dists(lists[curlist])); + w->update_permamsg(curlist, lists.size()); + + }); + QObject::connect(w, &MinGuiWidget::prev, + []{ + if (curlist > 0) --curlist; + w->show_images(build_list(lists[curlist])); + w->update_distances(build_dists(lists[curlist])); + w->update_permamsg(curlist, lists.size()); + }); + + a.exec(); + + return 0; +} diff --git a/mingui/mingui.cpp b/mingui/mingui.cpp new file mode 100644 index 0000000..b421512 --- /dev/null +++ b/mingui/mingui.cpp @@ -0,0 +1,279 @@ +#include "mingui.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using std::size_t; +const std::vector keys = { + Qt::Key::Key_A, Qt::Key::Key_S, Qt::Key::Key_D, Qt::Key::Key_F, + Qt::Key::Key_G, Qt::Key::Key_H, Qt::Key::Key_J, Qt::Key::Key_K, + Qt::Key::Key_L, Qt::Key::Key_Semicolon, Qt::Key::Key_T, Qt::Key::Key_Y, + Qt::Key::Key_U, Qt::Key::Key_I, Qt::Key::Key_O, Qt::Key::Key_P +}; + +MinGuiWidget::MinGuiWidget() +{ + this->setFont(QFontDatabase::systemFont(QFontDatabase::SystemFont::FixedFont)); + this->setWindowTitle("deduper minigui"); + this->setLayout(new QVBoxLayout(this)); + QWidget *c = new QWidget(this); + sb = new QStatusBar(this); + sb->addPermanentWidget(permamsg = new QLabel()); + QLabel *opm = new QLabel(); + opm->setText("z: previous group, m: next group, x: mark all for deletion, c: unmark all, click: toggle, shift+click: open, shift+return: save list"); + sb->addWidget(opm); + this->layout()->addWidget(c); + this->layout()->addWidget(sb); + l = new QHBoxLayout(c); + c->setLayout(l); + infopanel = new QLabel(this); + imgcontainer = new QWidget(this); + l->addWidget(imgcontainer); + l->addWidget(infopanel); + marked.clear(); + infopanel->setText("bleh"); + infopanel->setSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Minimum); +} + +void MinGuiWidget::show_images(const std::vector &fns) +{ + current_set = fns; + marks.clear(); + imgw.clear(); + qDeleteAll(imgcontainer->children()); + imgcontainer->setLayout(new QVBoxLayout(imgcontainer)); + int max_height = (this->screen()->size().height() / fns.size() * 0.75 - 24) * this->screen()->devicePixelRatio(); + int max_width = this->screen()->size().width() * 0.8 * this->screen()->devicePixelRatio(); + std::string common_pfx = common_prefix(fns); + size_t idx = 0; + if (fns.size() > keys.size()) + QMessageBox::warning(this, "Too many duplicates", "Too many duplicates found. Some couldn't be assigned a hotkey."); + for (auto &f : fns) + { + marks.push_back(marked.find(f) != marked.end()); + ImageWidget *tw = new ImageWidget(f, f.substr(common_pfx.length()), idx, max_width, max_height, this); + QObject::connect(tw, &ImageWidget::clicked, [this, idx] { this->mark_toggle(idx); }); + imgw.push_back(tw); + imgcontainer->layout()->addWidget(tw); + ++idx; + } + mark_view_update(false); +} + +void MinGuiWidget::update_distances(const std::map, double> &d) +{ + QString r; + for (auto &p : d) + { + QString ka = "(No hotkey)"; + QString kb = "(No hotkey)"; + if (p.first.first < keys.size()) + ka = QKeySequence(keys[p.first.first]).toString(); + if (p.first.second < keys.size()) + kb = QKeySequence(keys[p.first.second]).toString(); + r += QString("%1 <-> %2: %3\n").arg(ka).arg(kb).arg(QString::number(p.second)); + } + infopanel->setText(r); +} + +void MinGuiWidget::update_permamsg(std::size_t cur, std::size_t size) +{ + permamsg->setText(QString("Viewing group %1 of %2").arg(cur + 1).arg(size)); +} + +void MinGuiWidget::save_list() +{ + QString fn = QFileDialog::getSaveFileName(this, "Save list", QString(), "*.txt"); + FILE *f = fopen(fn.toStdString().c_str(), "w"); + for (auto &x : this->marked) + fprintf(f, "%s\n", x.c_str()); + fclose(f); +} + +void MinGuiWidget::mark_toggle(size_t x) +{ + if (x < marks.size()) + { + marks[x] = !marks[x]; + if (marks[x]) + marked.insert(current_set[x]); + else + if (marked.find(current_set[x]) != marked.end()) + marked.erase(marked.find(current_set[x])); + } + mark_view_update(); +} + +void MinGuiWidget::mark_all_but(size_t x) +{ + if (x < marks.size()) + { + for (size_t i = 0; i < marks.size(); ++i) + { + marks[i] = (i != x); + if (marks[i]) + marked.insert(current_set[i]); + else + if (marked.find(current_set[i]) != marked.end()) + marked.erase(marked.find(current_set[i])); + } + } + mark_view_update(); +} + +void MinGuiWidget::mark_all() +{ + for (size_t i = 0; i < marks.size(); ++i) + { + marks[i] = true; + marked.insert(current_set[i]); + } + mark_view_update(); +} + +void MinGuiWidget::mark_none() +{ + for (size_t i = 0; i < marks.size(); ++i) + { + marks[i] = false; + if (marked.find(current_set[i]) != marked.end()) + marked.erase(marked.find(current_set[i])); + } + mark_view_update(); +} + +void MinGuiWidget::mark_view_update(bool update_msg) +{ + size_t m = 0; + for (size_t i = 0; i < current_set.size(); ++i) + { + QPalette p = imgw[i]->palette(); + if (marks[i]) + { + p.setColor(QPalette::ColorRole::Window, Qt::GlobalColor::red); + ++m; + } + else + p.setColor(QPalette::ColorRole::Window, this->palette().color(QPalette::ColorRole::Window)); + imgw[i]->setBackgroundRole(QPalette::ColorRole::Window); + imgw[i]->setAutoFillBackground(true); + imgw[i]->setPalette(p); + } + if (update_msg) + sb->showMessage(QString("%1 of %2 marked for deletion").arg(m).arg(current_set.size()), 1000); +} + +std::string MinGuiWidget::common_prefix(const std::vector &fns) +{ + std::string ret; + std::string shortest = *std::min_element(fns.begin(), fns.end(), [](auto &a, auto &b){return a.length() < b.length();}); + for (size_t i = 0; i < shortest.length(); ++i) + { + char c = shortest[i]; + bool t = true; + for (auto &s : fns) if (s[i] != c) {t = false; break;} + if (!t) break; + ret.push_back(c); + } + if (!ret.empty()) + { + auto p = ret.rfind((char)std::filesystem::path::preferred_separator); + if (p != std::string::npos) + return ret.substr(0, p + 1); + } + return ret; +} + +void MinGuiWidget::keyPressEvent(QKeyEvent *e) +{ + for (auto &k : keys) + if (e->key() == k) + { + if (e->modifiers() & Qt::KeyboardModifier::ShiftModifier) + this->mark_all_but(&k - &keys[0]); + else this->mark_toggle(&k - &keys[0]); + } + switch (e->key()) + { + case Qt::Key::Key_X: this->mark_all(); break; + case Qt::Key::Key_C: this->mark_none(); break; + } +} + +void MinGuiWidget::keyReleaseEvent(QKeyEvent *e) +{ + switch (e->key()) + { + case Qt::Key::Key_M: Q_EMIT next(); break; + case Qt::Key::Key_Z: Q_EMIT prev(); break; + case Qt::Key::Key_Return: if (e->modifiers() & Qt::KeyboardModifier::ShiftModifier) save_list(); break; + } +} + +ImageWidget::ImageWidget(std::string f, std::string dispf, size_t _idx, int max_width, int max_height, QWidget *par) + : QWidget(par), fn(QString::fromStdString(f)), idx(_idx) +{ + this->setLayout(new QVBoxLayout(this)); + this->layout()->setMargin(10); + im = new QLabel(this); + this->layout()->addWidget(im); + QFile imgf(QString::fromStdString(f)); + QPixmap pm(QString::fromStdString(f)); + int imw = pm.width(); + int imh = pm.height(); + pm.setDevicePixelRatio(this->screen()->devicePixelRatio()); + if (pm.width() > max_width || pm.height() > max_height) + pm = pm.scaled(max_width, max_height, Qt::AspectRatioMode::KeepAspectRatio, Qt::TransformationMode::SmoothTransformation); + im->setPixmap(pm); + im->setSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Expanding); + lb = new QLabel(this); + this->layout()->addWidget(lb); + QString s = QString("<%1>: %2, %3 x %4, %5") + .arg(idx < keys.size() ? QKeySequence(keys[idx]).toString(): QString("(No hotkey available)")) + .arg(QString::fromStdString(dispf/*f.substr(common_pfx.length())*/)) + .arg(imw).arg(imh) + .arg(QLocale::system().formattedDataSize(imgf.size(), 3)); + lb->setTextFormat(Qt::TextFormat::PlainText); + lb->setText(s); + lb->setSizePolicy(QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Preferred); +} + +void ImageWidget::set_marked(bool marked) +{ + QPalette p = this->palette(); + if (marked) + p.setColor(QPalette::ColorRole::Window, Qt::GlobalColor::red); + else + p.setColor(QPalette::ColorRole::Window, qobject_cast(parent())->palette().color(QPalette::ColorRole::Window)); + this->setBackgroundRole(QPalette::ColorRole::Window); + this->setAutoFillBackground(true); + this->setPalette(p); +} + +void ImageWidget::mouseReleaseEvent(QMouseEvent *event) +{ + if (event->button() == Qt::MouseButton::LeftButton) + { + if (event->modifiers() & Qt::KeyboardModifier::ShiftModifier) + QDesktopServices::openUrl(QUrl::fromLocalFile(fn)); + else + Q_EMIT clicked(); + } +} diff --git a/mingui/mingui.hpp b/mingui/mingui.hpp new file mode 100644 index 0000000..7ed0eb1 --- /dev/null +++ b/mingui/mingui.hpp @@ -0,0 +1,64 @@ +#ifndef MINGUI_HPP +#define MINGUI_HPP + +#include +#include +#include + +#include + +class QHBoxLayout; +class QLabel; +class QStatusBar; + +class MinGuiWidget : public QWidget +{ + Q_OBJECT +private: + QHBoxLayout *l; + QLabel *infopanel; + QLabel *permamsg; + QWidget *imgcontainer; + QStatusBar *sb; + void mark_toggle(std::size_t x); + void mark_all_but(std::size_t x); + void mark_all(); + void mark_none(); + void mark_view_update(bool update_msg = true); + std::string common_prefix(const std::vector &fns); + std::vector imgw; + std::vector marks; + std::unordered_set marked; + std::vector current_set; +protected: + void keyPressEvent(QKeyEvent *e) override; + void keyReleaseEvent(QKeyEvent *e) override; +public: + MinGuiWidget(); + void show_images(const std::vector &fns); + void update_distances(const std::map, double> &d); + void update_permamsg(std::size_t cur, std::size_t size); + void save_list(); +Q_SIGNALS: + void next(); + void prev(); +}; + +class ImageWidget : public QWidget +{ + Q_OBJECT +private: + QString fn; + std::size_t idx; + QLabel *im; + QLabel *lb; +protected: + void mouseReleaseEvent(QMouseEvent *event) override; +public: + ImageWidget(std::string f, std::string dispfn, std::size_t _idx, int max_width, int max_height, QWidget *par); + void set_marked(bool marked); +Q_SIGNALS: + void clicked(); +}; + +#endif -- cgit v1.2.3