diff options
Diffstat (limited to 'qdeduper')
-rw-r--r-- | qdeduper/CMakeLists.txt | 18 | ||||
-rw-r--r-- | qdeduper/README.md | 7 | ||||
-rw-r--r-- | qdeduper/imageitem.cpp | 136 | ||||
-rw-r--r-- | qdeduper/imageitem.hpp | 42 | ||||
-rw-r--r-- | qdeduper/main.cpp | 29 | ||||
-rw-r--r-- | qdeduper/mingui.cpp | 404 | ||||
-rw-r--r-- | qdeduper/mingui.hpp | 65 |
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 |