aboutsummaryrefslogtreecommitdiff
path: root/mingui
diff options
context:
space:
mode:
authorGravatar Chris Xiong <chirs241097@gmail.com> 2022-08-29 00:30:57 -0400
committerGravatar Chris Xiong <chirs241097@gmail.com> 2022-08-29 00:30:57 -0400
commit67be79385f0b22fe6428e213d2b6422742d994c4 (patch)
treeeb80c9bf3048ca9b0775b756bbc40449203e105e /mingui
parenteb30f55ccce35a258d27f12ab036bf63453ac3da (diff)
downloaddeduper-67be79385f0b22fe6428e213d2b6422742d994c4.tar.xz
Add mingui.
Diffstat (limited to 'mingui')
-rw-r--r--mingui/CMakeLists.txt24
-rw-r--r--mingui/README.md7
-rw-r--r--mingui/main.cpp150
-rw-r--r--mingui/mingui.cpp279
-rw-r--r--mingui/mingui.hpp64
5 files changed, 524 insertions, 0 deletions
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 <cstdio>
+#include <algorithm>
+#include <map>
+#include <string>
+#include <vector>
+#include <unordered_map>
+#include <utility>
+
+#include <QWidget>
+#include <QApplication>
+#include "mingui.hpp"
+
+using std::size_t;
+
+std::unordered_map<std::string, size_t> p;
+std::vector<std::string> fns;
+std::map<std::pair<size_t, size_t>, double> dist;
+std::vector<size_t> par;
+std::vector<std::vector<size_t>> 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<std::string> build_list(const std::vector<size_t> &l)
+{
+ std::vector<std::string> ret;
+ for (auto &x : l)
+ ret.push_back(fns[x]);
+ return ret;
+}
+
+std::map<std::pair<size_t, size_t>, double> build_dists(const std::vector<size_t> &l)
+{
+ std::map<std::pair<size_t, size_t>, 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 <cstdio>
+#include <filesystem>
+
+#include <QDebug>
+#include <QKeyEvent>
+#include <QString>
+#include <QLabel>
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+#include <QStatusBar>
+#include <QPixmap>
+#include <QFile>
+#include <QScreen>
+#include <QFont>
+#include <QFontDatabase>
+#include <QFileDialog>
+#include <QKeySequence>
+#include <QMessageBox>
+#include <QDesktopServices>
+
+using std::size_t;
+const std::vector<int> 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<std::string> &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<std::pair<size_t, size_t>, 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<std::string> &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<QWidget*>(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 <vector>
+#include <string>
+#include <unordered_set>
+
+#include <QWidget>
+
+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<std::string> &fns);
+ std::vector<QWidget*> imgw;
+ std::vector<bool> marks;
+ std::unordered_set<std::string> marked;
+ std::vector<std::string> current_set;
+protected:
+ void keyPressEvent(QKeyEvent *e) override;
+ void keyReleaseEvent(QKeyEvent *e) override;
+public:
+ MinGuiWidget();
+ void show_images(const std::vector<std::string> &fns);
+ void update_distances(const std::map<std::pair<std::size_t, std::size_t>, 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