From 4b8d314f575d9e893d8dda7431194f8b470fc888 Mon Sep 17 00:00:00 2001 From: Chris Xiong Date: Sun, 18 Sep 2022 11:08:01 -0400 Subject: First step to adopt mingui as part of the project -- break it! --- qdeduper/mingui.cpp | 404 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 404 insertions(+) create mode 100644 qdeduper/mingui.cpp (limited to 'qdeduper/mingui.cpp') 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 +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#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 +}; + +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(); + 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(); + 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(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 &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, 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 &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(); +} -- cgit v1.2.3