aboutsummaryrefslogtreecommitdiff
path: root/qdeduper
diff options
context:
space:
mode:
Diffstat (limited to 'qdeduper')
-rw-r--r--qdeduper/CMakeLists.txt18
-rw-r--r--qdeduper/README.md7
-rw-r--r--qdeduper/imageitem.cpp136
-rw-r--r--qdeduper/imageitem.hpp42
-rw-r--r--qdeduper/main.cpp29
-rw-r--r--qdeduper/mingui.cpp404
-rw-r--r--qdeduper/mingui.hpp65
7 files changed, 701 insertions, 0 deletions
diff --git a/qdeduper/CMakeLists.txt b/qdeduper/CMakeLists.txt
new file mode 100644
index 0000000..f862c16
--- /dev/null
+++ b/qdeduper/CMakeLists.txt
@@ -0,0 +1,18 @@
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+find_package(Qt5 REQUIRED COMPONENTS Widgets)
+
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_AUTORCC ON)
+set(CMAKE_AUTOUIC ON)
+
+add_executable(qdeduper
+ main.cpp
+ mingui.cpp
+ imageitem.cpp
+)
+
+target_link_libraries(qdeduper
+ xsig
+ Qt5::Widgets
+)
diff --git a/qdeduper/README.md b/qdeduper/README.md
new file mode 100644
index 0000000..dff674f
--- /dev/null
+++ b/qdeduper/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/qdeduper/imageitem.cpp b/qdeduper/imageitem.cpp
new file mode 100644
index 0000000..6fee930
--- /dev/null
+++ b/qdeduper/imageitem.cpp
@@ -0,0 +1,136 @@
+#include "imageitem.hpp"
+
+#include <algorithm>
+
+#include <QDebug>
+#include <QFileInfo>
+#include <QPixmap>
+#include <QListView>
+#include <QFontMetrics>
+#include <QPainter>
+#include <QLocale>
+
+ImageItem::ImageItem(QString fn, QString dispn, QKeySequence hotkey, double pxratio)
+{
+ this->setText(dispn);
+ this->setData(fn, ImageItemRoles::path_role);
+ this->setData(QFileInfo(fn).size(), ImageItemRoles::file_size_role);
+ this->setCheckable(true);
+ QPixmap pm(fn);
+ pm.setDevicePixelRatio(pxratio);
+ this->setData(pm.size(), ImageItemRoles::dimension_role);
+ this->setData(hotkey, ImageItemRoles::hotkey_role);
+ this->setData(pm, Qt::ItemDataRole::DecorationRole);
+}
+
+void ImageItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+ QRect imr = option.rect;
+ imr.adjust(MARGIN + BORDER, MARGIN + BORDER, -MARGIN - BORDER, -MARGIN - BORDER);
+ QFont bfnt(option.font);
+ bfnt.setBold(true);
+ QFontMetrics bfm(bfnt);
+ imr.adjust(0, 0, 0, -2 * HKPADD - bfm.height() - LINESP);
+
+ QRect selr = option.rect.adjusted(MARGIN, MARGIN, -MARGIN, -MARGIN);
+ QStyleOptionViewItem so(option);
+ so.rect = selr;
+ if (index.data(Qt::ItemDataRole::CheckStateRole).value<Qt::CheckState>() == Qt::CheckState::Checked)
+ so.state |= QStyle::StateFlag::State_Selected;
+ so.state |= QStyle::StateFlag::State_Active;
+ option.widget->style()->drawPrimitive(QStyle::PrimitiveElement::PE_PanelItemViewItem, &so, painter, option.widget);
+
+
+ QPixmap pm = index.data(Qt::ItemDataRole::DecorationRole).value<QPixmap>();
+ QSize imd = pm.size().scaled(imr.size(), Qt::AspectRatioMode::KeepAspectRatio);
+ painter->setRenderHint(QPainter::RenderHint::SmoothPixmapTransform);
+ painter->drawPixmap(QRect(imr.topLeft(), imd), pm);
+ QPoint dtopright = QRect(imr.topLeft(),imd).bottomLeft();
+
+ QPoint hko = dtopright + QPoint(HKPADD, HKPADD + LINESP);
+ QKeySequence ks = index.data(ImageItem::ImageItemRoles::hotkey_role).value<QKeySequence>();
+ QString kss = ks.toString();
+ QRect r = bfm.boundingRect(kss);
+ r.moveTopLeft(hko);
+ QRect hkbg = r.adjusted(-HKPADD, -HKPADD, HKPADD, HKPADD);
+ if (hkbg.width() < hkbg.height())
+ {
+ int shift = (hkbg.height() - hkbg.width()) / 2;
+ r.adjust(shift, 0, shift, 0);
+ hkbg.setWidth(hkbg.height());
+ }
+ painter->setPen(QPen(QBrush(), 0));
+ painter->setBrush(option.widget->palette().color(QPalette::ColorGroup::Normal, QPalette::ColorRole::Light));
+ painter->drawRoundedRect(hkbg.adjusted(HKSHDS, HKSHDS, HKSHDS, HKSHDS), 4, 4);
+ painter->setBrush(option.widget->palette().color(QPalette::ColorGroup::Normal, QPalette::ColorRole::WindowText));
+ painter->drawRoundedRect(hkbg, 4, 4);
+ painter->setPen(option.widget->palette().color(QPalette::ColorGroup::Normal, QPalette::ColorRole::Window));
+ painter->setBrush(QBrush());
+ painter->setFont(bfnt);
+ painter->drawText(r, kss);
+
+ QPoint ftopright = hkbg.topRight() + QPoint(LINESP + HKSHDS, 0);
+ QSize dim = index.data(ImageItem::ImageItemRoles::dimension_role).value<QSize>();
+ qint64 fsz = index.data(ImageItem::ImageItemRoles::file_size_role).value<qint64>();
+ QString infos = QString("%1 x %2, %3")
+ .arg(dim.width()).arg(dim.height())
+ .arg(QLocale::system().formattedDataSize(fsz, 3));
+ QString fns = index.data(Qt::ItemDataRole::DisplayRole).toString();
+ QTextOption topt;
+ topt.setWrapMode(QTextOption::WrapMode::NoWrap);
+ r = option.fontMetrics.boundingRect(infos);
+ r.moveTopLeft(ftopright + QPoint(0, (hkbg.height() - r.height()) / 2));
+ painter->setFont(option.font);
+ painter->setPen(option.widget->palette().color(QPalette::ColorGroup::Normal, QPalette::ColorRole::Text));
+ painter->drawText(r, infos, topt);
+ topt.setAlignment(Qt::AlignmentFlag::AlignRight);
+ r.setLeft(r.right());
+ r.setRight(imr.right());
+ QString efns = option.fontMetrics.elidedText(fns, Qt::TextElideMode::ElideMiddle, r.width());
+ painter->drawText(r, efns, topt);
+}
+
+QSize ImageItemDelegate::sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const
+{
+ const QListView *lw = qobject_cast<const QListView*>(option.widget);
+ QSize vpsz = lw->maximumViewportSize();
+ vpsz.setWidth(vpsz.width() - vw);
+ vpsz.setHeight(vpsz.height() - hh);
+ QPixmap pm = index.data(Qt::ItemDataRole::DecorationRole).value<QPixmap>();
+ QSize onscsz = pm.size() / pm.devicePixelRatioF();
+ int imh = onscsz.height();
+ if (onscsz.width() > vpsz.width() - 2 * MARGIN - 2 * BORDER)
+ imh = (vpsz.width() - 2 * MARGIN - 2 * BORDER) / (double)onscsz.width() * onscsz.height();
+
+ QFont fnt(option.font);
+ fnt.setBold(true);
+ QFontMetrics fm(fnt);
+ int extra_height = 2 * MARGIN + 2 * BORDER + LINESP + fm.height() + 2 * HKPADD + HKSHDS;
+ int min_height = 64;
+ int max_height = imh;
+
+ QSize dim = index.data(ImageItem::ImageItemRoles::dimension_role).value<QSize>();
+ qint64 fsz = index.data(ImageItem::ImageItemRoles::file_size_role).value<qint64>();
+ QString infos = QString("%1 x %2, %3")
+ .arg(dim.width()).arg(dim.height())
+ .arg(QLocale::system().formattedDataSize(fsz, 3));
+ int textw = option.fontMetrics.boundingRect(infos).width() + fm.height() + 2 * HKPADD + 48;
+
+ QSize ret(vpsz);
+ if (textw > vpsz.width()) ret.setWidth(textw);
+ ret.setHeight(vpsz.height() / index.model()->rowCount() - lw->spacing());
+ ret.setHeight(std::max(min_height + extra_height, ret.height()));
+ ret.setHeight(std::min(max_height + extra_height, ret.height()));
+ return ret;
+}
+
+void ImageItemDelegate::resize(const QModelIndex &index)
+{
+ Q_EMIT sizeHintChanged(index);
+}
+
+void ImageItemDelegate::setScrollbarMargins(int vw, int hh)
+{
+ this->vw = vw;
+ this->hh = hh;
+}
diff --git a/qdeduper/imageitem.hpp b/qdeduper/imageitem.hpp
new file mode 100644
index 0000000..43fb0c8
--- /dev/null
+++ b/qdeduper/imageitem.hpp
@@ -0,0 +1,42 @@
+#ifndef IMAGEITEM_HPP
+#define IMAGEITEM_HPP
+
+#include <QStandardItem>
+#include <QAbstractItemDelegate>
+#include <QStyleOptionViewItem>
+#include <QModelIndex>
+
+class ImageItem : public QStandardItem
+{
+public:
+ enum ImageItemRoles
+ {
+ path_role = Qt::ItemDataRole::UserRole + 0x1000,
+ dimension_role,
+ file_size_role,
+ hotkey_role
+ };
+ ImageItem(QString fn, QString dispn, QKeySequence hotkey, double pxratio = 1.0);
+};
+
+class ImageItemDelegate : public QAbstractItemDelegate
+{
+ Q_OBJECT
+private:
+ const static int MARGIN = 3;
+ const static int BORDER = 3;
+ const static int HKPADD = 4;
+ const static int LINESP = 4;
+ const static int HKSHDS = 2;
+ int vw = -1;
+ int hh = -1;
+public:
+ void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const;
+ QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const;
+ void resize(const QModelIndex &index);
+ void setScrollbarMargins(int vw, int hh);
+Q_SIGNALS:
+ void sizeHintChanged(const QModelIndex &index);
+};
+
+#endif
diff --git a/qdeduper/main.cpp b/qdeduper/main.cpp
new file mode 100644
index 0000000..9b40ea4
--- /dev/null
+++ b/qdeduper/main.cpp
@@ -0,0 +1,29 @@
+#include <cstdio>
+#include <algorithm>
+#include <filesystem>
+#include <map>
+#include <string>
+#include <vector>
+#include <unordered_map>
+#include <utility>
+
+#include <QWidget>
+#include <QApplication>
+#include "mingui.hpp"
+
+using std::size_t;
+namespace fs = std::filesystem;
+
+DeduperMainWindow *w = nullptr;
+
+int main(int argc, char **argv)
+{
+ QApplication a(argc, argv);
+
+ w = new DeduperMainWindow();
+ w->show();
+
+ a.exec();
+
+ return 0;
+}
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();
+}
diff --git a/qdeduper/mingui.hpp b/qdeduper/mingui.hpp
new file mode 100644
index 0000000..b8d13ef
--- /dev/null
+++ b/qdeduper/mingui.hpp
@@ -0,0 +1,65 @@
+#ifndef MINGUI_HPP
+#define MINGUI_HPP
+
+#include <filesystem>
+#include <vector>
+#include <string>
+#include <unordered_set>
+
+#include <QMainWindow>
+#include <QList>
+
+class QHBoxLayout;
+class QLabel;
+class QStatusBar;
+class QScrollArea;
+class QTextEdit;
+class QListView;
+class QSplitter;
+class QStandardItemModel;
+class ImageItemDelegate;
+
+namespace fs = std::filesystem;
+
+class DeduperMainWindow : public QMainWindow
+{
+ Q_OBJECT
+private:
+ QSplitter *l;
+ QTextEdit *infopanel;
+ QLabel *permamsg;
+ QStatusBar *sb;
+ QListView *lw;
+ QList<QAction*> selhk;
+ QStandardItemModel *im = nullptr;
+ ImageItemDelegate *id = nullptr;
+ std::size_t ngroups, curgroup;
+ bool nohotkeywarn;
+ 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);
+ fs::path::string_type common_prefix(const std::vector<fs::path> &fns);
+ std::vector<bool> marks;
+ std::unordered_set<fs::path> marked;
+ std::vector<fs::path> current_set;
+protected:
+ void resizeEvent(QResizeEvent *e) override;
+ void closeEvent(QCloseEvent *e) override;
+public:
+ DeduperMainWindow();
+
+ void setup_menu();
+ void show_images(const std::vector<std::filesystem::path> &fns);
+ void update_distances(const std::map<std::pair<std::size_t, std::size_t>, double> &d);
+ void update_viewstatus(std::size_t cur, std::size_t size);
+ void save_list();
+ void load_list();
+Q_SIGNALS:
+ void next();
+ void prev();
+ void switch_group(std::size_t group);
+};
+
+#endif