aboutsummaryrefslogtreecommitdiff
path: root/qdeduper/mingui.cpp
diff options
context:
space:
mode:
authorGravatar Chris Xiong <chirs241097@gmail.com> 2022-09-18 11:08:01 -0400
committerGravatar Chris Xiong <chirs241097@gmail.com> 2022-09-18 11:08:01 -0400
commit4b8d314f575d9e893d8dda7431194f8b470fc888 (patch)
tree4d42ebc06d2522c9141c42ff91be214e521e5fcb /qdeduper/mingui.cpp
parent0570b0f172631ba8f3c6180d2c850635c3cd6037 (diff)
downloaddeduper-4b8d314f575d9e893d8dda7431194f8b470fc888.tar.xz
First step to adopt mingui as part of the project -- break it!
Diffstat (limited to 'qdeduper/mingui.cpp')
-rw-r--r--qdeduper/mingui.cpp404
1 files changed, 404 insertions, 0 deletions
diff --git a/qdeduper/mingui.cpp b/qdeduper/mingui.cpp
new file mode 100644
index 0000000..317076c
--- /dev/null
+++ b/qdeduper/mingui.cpp
@@ -0,0 +1,404 @@
+#include "mingui.hpp"
+#include "imageitem.hpp"
+
+#include <cstdio>
+#include <cwchar>
+#include <type_traits>
+
+#include <QDebug>
+#include <QCloseEvent>
+#include <QMouseEvent>
+#include <QScrollBar>
+#include <QMenuBar>
+#include <QMenu>
+#include <QAction>
+#include <QSplitter>
+#include <QString>
+#include <QScrollArea>
+#include <QListView>
+#include <QStandardItemModel>
+#include <QLabel>
+#include <QHBoxLayout>
+#include <QVBoxLayout>
+#include <QStatusBar>
+#include <QPixmap>
+#include <QFile>
+#include <QScreen>
+#include <QFont>
+#include <QFontDatabase>
+#include <QFileDialog>
+#include <QKeySequence>
+#include <QTextEdit>
+#include <QMessageBox>
+#include <QInputDialog>
+#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
+};
+
+QString fsstr_to_qstring(const fs::path::string_type &s)
+{
+#if PATH_VALSIZE == 2 //the degenerate platform
+ return QString::fromStdWString(s);
+#else
+ return QString::fromStdString(s);
+#endif
+}
+
+DeduperMainWindow::DeduperMainWindow()
+{
+ this->setFont(QFontDatabase::systemFont(QFontDatabase::SystemFont::FixedFont));
+ this->setWindowTitle("deduper");
+ this->setup_menu();
+ sb = this->statusBar();
+ sb->addPermanentWidget(permamsg = new QLabel());
+ QLabel *opm = new QLabel();
+ opm->setText("placeholder status bar text");
+ sb->addWidget(opm);
+ l = new QSplitter(Qt::Orientation::Horizontal, this);
+ l->setContentsMargins(6, 6, 6, 6);
+ l->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
+ this->setCentralWidget(l);
+ infopanel = new QTextEdit(this);
+ infopanel->setReadOnly(true);
+ infopanel->setMinimumWidth(80);
+ lw = new QListView(this);
+ im = new QStandardItemModel(this);
+ lw->setModel(im);
+ lw->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOn);
+ lw->setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAlwaysOn);
+ id = new ImageItemDelegate();
+ id->setScrollbarMargins(lw->verticalScrollBar()->width(),
+ lw->horizontalScrollBar()->height());
+ lw->setItemDelegate(id);
+ lw->setSelectionMode(QAbstractItemView::SelectionMode::NoSelection);
+ lw->setResizeMode(QListView::ResizeMode::Adjust);
+ lw->setVerticalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
+ lw->setHorizontalScrollBarPolicy(Qt::ScrollBarPolicy::ScrollBarAsNeeded);
+ lw->setHorizontalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel);
+ lw->setVerticalScrollMode(QAbstractItemView::ScrollMode::ScrollPerPixel);
+ lw->setMinimumWidth(240);
+
+ for (size_t i = 0; i < keys.size(); ++i)
+ {
+ auto &k = keys[i];
+ QAction *a = new QAction();
+ a->setShortcut(QKeySequence(k));
+ QObject::connect(a, &QAction::triggered, [this, i](){this->mark_toggle(i);});
+ selhk.push_back(a);
+ QAction *sa = new QAction();
+ sa->setShortcut(QKeySequence(Qt::Modifier::SHIFT | k));
+ QObject::connect(sa, &QAction::triggered, [this, i](){this->mark_all_but(i);});
+ selhk.push_back(a);
+ }
+ this->addActions(selhk);
+ QAction *mall = new QAction();
+ mall->setShortcut(QKeySequence(Qt::Key::Key_X));
+ QObject::connect(mall, &QAction::triggered, [this]{this->mark_all();});
+ this->addAction(mall);
+ QAction *mnone = new QAction();
+ mnone->setShortcut(QKeySequence(Qt::Key::Key_C));
+ QObject::connect(mnone, &QAction::triggered, [this]{this->mark_none();});
+ this->addAction(mnone);
+ QAction *nxt = new QAction();
+ nxt->setShortcut(QKeySequence(Qt::Key::Key_M));
+ QObject::connect(nxt, &QAction::triggered, [this]{Q_EMIT this->next();});
+ this->addAction(nxt);
+ QAction *prv = new QAction();
+ prv->setShortcut(QKeySequence(Qt::Key::Key_Z));
+ QObject::connect(prv, &QAction::triggered, [this]{Q_EMIT this->prev();});
+ this->addAction(prv);
+ QAction *load = new QAction();
+ load->setShortcut(QKeySequence(Qt::Key::Key_N));
+ QObject::connect(load, &QAction::triggered, [this]{Q_EMIT this->load_list();});
+ this->addAction(load);
+ QAction *skip = new QAction();
+ skip->setShortcut(QKeySequence(Qt::Key::Key_B));
+ QObject::connect(skip, &QAction::triggered, [this]{
+ bool ok = false;
+ int g = QInputDialog::getInt(this, "Skip to group",
+ QString("Group # (1-%1)").arg(ngroups),
+ curgroup + 1,
+ 1, ngroups, 1, &ok);
+ if (ok) Q_EMIT switch_group((size_t) g - 1);
+ });
+ this->addAction(skip);
+ QAction *save = new QAction();
+ save->setShortcut(QKeySequence(Qt::Modifier::SHIFT | Qt::Key::Key_Return));
+ QObject::connect(save, &QAction::triggered, [this]{Q_EMIT this->save_list();});
+ this->addAction(save);
+
+ QObject::connect(lw, &QListView::clicked, [this](const QModelIndex &i) {
+ auto cs = i.data(Qt::ItemDataRole::CheckStateRole).value<Qt::CheckState>();
+ if (cs == Qt::CheckState::Checked)
+ cs = Qt::CheckState::Unchecked;
+ else cs = Qt::CheckState::Checked;
+ this->im->setData(i, cs, Qt::ItemDataRole::CheckStateRole);
+ });
+ QObject::connect(lw, &QListView::doubleClicked, [this](const QModelIndex &i) {
+ auto cs = i.data(Qt::ItemDataRole::CheckStateRole).value<Qt::CheckState>();
+ if (cs == Qt::CheckState::Checked)
+ cs = Qt::CheckState::Unchecked;
+ else cs = Qt::CheckState::Checked;
+ this->im->setData(i, cs, Qt::ItemDataRole::CheckStateRole);
+ QDesktopServices::openUrl(QUrl::fromLocalFile(i.data(ImageItem::ImageItemRoles::path_role).toString()));
+ });
+ QObject::connect(im, &QStandardItemModel::itemChanged, [this](QStandardItem *i) {
+ ImageItem *itm = static_cast<ImageItem*>(i);
+ QModelIndex idx = itm->index();
+ bool checked = itm->data(Qt::ItemDataRole::CheckStateRole) == Qt::CheckState::Checked;
+ if (checked != marks[idx.row()])
+ this->mark_toggle(idx.row());
+ });
+ l->addWidget(lw);
+ l->addWidget(infopanel);
+ l->setStretchFactor(0, 3);
+ l->setStretchFactor(1, 1);
+ l->setCollapsible(0, false);
+ marked.clear();
+ infopanel->setText("bleh");
+ infopanel->setSizePolicy(QSizePolicy::Policy::Minimum, QSizePolicy::Policy::Minimum);
+ nohotkeywarn = false;
+}
+
+void DeduperMainWindow::setup_menu()
+{
+ QMenu *file = this->menuBar()->addMenu("File");
+ QMenu *view = this->menuBar()->addMenu("View");
+ QMenu *mark = this->menuBar()->addMenu("Marks");
+ QMenu *help = this->menuBar()->addMenu("Help");
+
+ file->addAction("Create Database...");
+ file->addAction("Load Database...");
+ file->addAction("Save Database...");
+ file->addSeparator();
+ file->addAction("Search for Image...");
+ file->addSeparator();
+ file->addAction("Preferences...");
+ file->addAction("Exit");
+
+ view->addAction("Next Group");
+ view->addAction("Previous Group");
+ view->addSeparator();
+ QMenu *sort = view->addMenu("Sort by");
+ sort->addAction("File size");
+ sort->addAction("Image dimension");
+ sort->addAction("File path");
+
+ mark->addAction("Mark All");
+ mark->addAction("Mark None");
+ mark->addAction("Mark All within...");
+ mark->addAction("Review Marked Imagess");
+
+ help->addAction("View Documentation");
+ help->addAction("About");
+}
+
+void DeduperMainWindow::show_images(const std::vector<fs::path> &fns)
+{
+ current_set = fns;
+ marks.clear();
+ im->clear();
+ int max_height = (this->screen()->size().height() / fns.size() * 0.8 - 24) * this->screen()->devicePixelRatio();
+ int max_width = this->screen()->size().width() * 0.8 * this->screen()->devicePixelRatio();
+ if (max_height < 64) max_height = 64;
+ if (max_width < 64) max_width = 64;
+ fs::path::string_type common_pfx = common_prefix(fns);
+ size_t idx = 0;
+ if (fns.size() > keys.size() && !nohotkeywarn)
+ nohotkeywarn = QMessageBox::StandardButton::Ignore ==
+ QMessageBox::warning(this,
+ "Too many duplicates",
+ "Too many duplicates found. Some couldn't be assigned a hotkey. Ignore = do not show again.",
+ QMessageBox::StandardButton::Ok | QMessageBox::StandardButton::Ignore,
+ QMessageBox::StandardButton::Ok);
+ for (auto &f : fns)
+ {
+ marks.push_back(marked.find(f) != marked.end());
+ im->appendRow(new ImageItem(fsstr_to_qstring(f.native()), fsstr_to_qstring(f.native().substr(common_pfx.length())), keys[idx], lw->devicePixelRatioF()));
+ ++idx;
+ }
+ mark_view_update(false);
+}
+
+void DeduperMainWindow::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 DeduperMainWindow::update_viewstatus(std::size_t cur, std::size_t size)
+{
+ permamsg->setText(QString("Viewing group %1 of %2").arg(cur + 1).arg(size));
+ ngroups = size;
+ curgroup = cur;
+}
+
+void DeduperMainWindow::save_list()
+{
+ QString fn = QFileDialog::getSaveFileName(this, "Save list", QString(), "*.txt");
+ FILE *f = fopen(fn.toStdString().c_str(), "w");
+ if (!f) return;
+ for (auto &x : this->marked)
+#ifdef _WIN32
+ fwprintf(f, L"%ls\n", x.c_str());
+#else
+ fprintf(f, "%s\n", x.c_str());
+#endif
+ fclose(f);
+}
+
+void DeduperMainWindow::load_list()
+{
+ QString fn = QFileDialog::getOpenFileName(this, "Load list", QString(), "*.txt");
+ FILE *f = fopen(fn.toStdString().c_str(), "r");
+ if (!f) return;
+ this->marked.clear();
+ while(!feof(f))
+ {
+#ifdef _WIN32
+ wchar_t buf[32768];
+ fgetws(buf, 32768, f);
+ std::wstring ws(buf);
+ if (ws.back() == L'\n') ws.pop_back();
+ if (!ws.empty()) this->marked.insert(ws);
+#else
+ char buf[32768];
+ fgets(buf, 32768, f);
+ std::string s(buf);
+ if (s.back() == '\n') s.pop_back();
+ if (!s.empty()) this->marked.insert(s);
+#endif
+ }
+ fclose(f);
+ for (size_t i = 0; i < marks.size(); ++i)
+ marks[i] = marked.find(current_set[i]) != marked.end();
+ mark_view_update();
+}
+
+void DeduperMainWindow::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 DeduperMainWindow::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 DeduperMainWindow::mark_all()
+{
+ for (size_t i = 0; i < marks.size(); ++i)
+ {
+ marks[i] = true;
+ marked.insert(current_set[i]);
+ }
+ mark_view_update();
+}
+
+void DeduperMainWindow::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 DeduperMainWindow::mark_view_update(bool update_msg)
+{
+ size_t m = 0;
+ for (size_t i = 0; i < current_set.size(); ++i)
+ {
+ if (marks[i])
+ {
+ im->item(i)->setCheckState(Qt::CheckState::Checked);
+ ++m;
+ }
+ else
+ {
+ im->item(i)->setCheckState(Qt::CheckState::Unchecked);
+ }
+ }
+ if (update_msg)
+ sb->showMessage(QString("%1 of %2 marked for deletion").arg(m).arg(current_set.size()), 1000);
+}
+
+fs::path::string_type DeduperMainWindow::common_prefix(const std::vector<fs::path> &fns)
+{
+ using fsstr = fs::path::string_type;
+ fsstr ret;
+ fsstr shortest = *std::min_element(fns.begin(), fns.end(), [](auto &a, auto &b){return a.native().length() < b.native().length();});
+ for (size_t i = 0; i < shortest.length(); ++i)
+ {
+ fs::path::value_type c = shortest[i];
+ bool t = true;
+ for (auto &s : fns) if (s.c_str()[i] != c) {t = false; break;}
+ if (!t) break;
+ ret.push_back(c);
+ }
+ if (!ret.empty())
+ {
+ auto p = ret.rfind(std::filesystem::path::preferred_separator);
+ if (p != fsstr::npos)
+ return fs::path(ret.substr(0, p + 1));
+ }
+ return ret;
+}
+
+void DeduperMainWindow::resizeEvent(QResizeEvent *e)
+{
+ QWidget::resizeEvent(e);
+ if (!id || !im) return;
+ for (int i = 0; i < im->rowCount(); ++i)
+ id->resize(im->indexFromItem(im->item(i)));
+}
+
+void DeduperMainWindow::closeEvent(QCloseEvent *e)
+{
+ if (QMessageBox::StandardButton::Yes ==
+ QMessageBox::question(this, "Confirmation", "Really quit?",
+ QMessageBox::StandardButton::Yes | QMessageBox::StandardButton::No,
+ QMessageBox::StandardButton::No))
+ e->accept();
+ else
+ e->ignore();
+}