From e25c84c0463f5a43d3b2bb836850f5c5963a2846 Mon Sep 17 00:00:00 2001 From: Chris Xiong Date: Fri, 30 Sep 2022 23:58:46 -0400 Subject: Add context menu for the image view. Shortcuts are currently broken. Will be fixed in future commits. --- qdeduper/CMakeLists.txt | 15 +++- qdeduper/mingui.cpp | 180 ++++++++++++++++++++++++++++++------------------ qdeduper/mingui.hpp | 13 ++++ qdeduper/utilities.cpp | 65 +++++++++++++++++ qdeduper/utilities.hpp | 18 +++++ 5 files changed, 223 insertions(+), 68 deletions(-) create mode 100644 qdeduper/utilities.cpp create mode 100644 qdeduper/utilities.hpp diff --git a/qdeduper/CMakeLists.txt b/qdeduper/CMakeLists.txt index 7373331..3adc4ba 100644 --- a/qdeduper/CMakeLists.txt +++ b/qdeduper/CMakeLists.txt @@ -3,9 +3,13 @@ set(CMAKE_INCLUDE_CURRENT_DIR ON) option(QDEDUPER_USE_QT6 "Build qdeduper with Qt 6" OFF) if (QDEDUPER_USE_QT6) - find_package(Qt6 REQUIRED COMPONENTS Widgets Concurrent) + find_package(Qt6 REQUIRED COMPONENTS Widgets Concurrent OPTIONAL_COMPONENTS DBus) else() - find_package(Qt5 REQUIRED COMPONENTS Widgets Concurrent) + find_package(Qt5 REQUIRED COMPONENTS Widgets Concurrent OPTIONAL_COMPONENTS DBus) +endif() + +if (Qt5DBus_FOUND OR Qt6DBus_FOUND) + add_compile_definitions(HAS_QTDBUS) endif() set(CMAKE_AUTOMOC ON) @@ -21,6 +25,7 @@ add_executable(qdeduper pathchooser.cpp settings.cpp preferencedialog.cpp + utilities.cpp resources.qrc ) @@ -30,6 +35,12 @@ target_link_libraries(qdeduper if (QDEDUPER_USE_QT6) target_link_libraries(qdeduper Qt6::Widgets Qt6::Concurrent) + if(Qt6DBus_FOUND) + target_link_libraries(qdeduper Qt6::DBus) + endif() else() target_link_libraries(qdeduper Qt5::Widgets Qt5::Concurrent) + if(Qt5DBus_FOUND) + target_link_libraries(qdeduper Qt5::DBus) + endif() endif() diff --git a/qdeduper/mingui.cpp b/qdeduper/mingui.cpp index 9296de7..e7ceeab 100644 --- a/qdeduper/mingui.cpp +++ b/qdeduper/mingui.cpp @@ -1,4 +1,5 @@ #include "mingui.hpp" +#include "utilities.hpp" #include "imageitem.hpp" #include "filescanner.hpp" #include "pathchooser.hpp" @@ -6,6 +7,7 @@ #include "settings.hpp" #include "preferencedialog.hpp" +#include #include #include #include @@ -48,7 +50,7 @@ #include #include -const std::vector keys = { +const std::vector iadefkeys = { 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, @@ -68,29 +70,18 @@ const std::map defhk = { {"10_skip_group", QKeySequence(Qt::Key::Key_B)}, {"11_single_mode_toggle", QKeySequence()}, {"12_mark_all", QKeySequence(Qt::Key::Key_X)}, - {"13_mark_all_dir", QKeySequence()}, - {"14_mark_all_dir_rec", QKeySequence()}, - {"15_view_marked", QKeySequence()}, + {"13_mark_none", QKeySequence(Qt::Key::Key_C)}, + {"14_mark_all_dir", QKeySequence()}, + {"15_mark_all_dir_rec", QKeySequence()}, + {"16_view_marked", QKeySequence()}, +}; +const std::vector iadefmo = { + 0, //mark_toggle + Qt::Modifier::SHIFT, //mark_all_except + Qt::Modifier::CTRL | Qt::Modifier::SHIFT, //show_only + Qt::Modifier::ALT | Qt::Modifier::SHIFT, //open_with_system_viewer + INT_MAX //open_containing_folder }; - - -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 -} - -fs::path qstring_to_path(const QString &s) -{ -#if PATH_VALSIZE == 2 //the degenerate platform - return fs::path(s.toStdWString()); -#else - return fs::path(s.toStdString()); -#endif -} Q_DECLARE_METATYPE(fs::path) @@ -99,8 +90,6 @@ DeduperMainWindow::DeduperMainWindow() qRegisterMetaType(); qApp->setWindowIcon(QIcon(":/img/deduper.png")); this->setWindowTitle("QDeduper"); - this->setup_menu(); - this->update_actions(); sb = this->statusBar(); sb->addPermanentWidget(dbramusg = new QLabel()); sb->addPermanentWidget(permamsg = new QLabel()); @@ -157,7 +146,10 @@ DeduperMainWindow::DeduperMainWindow() pdlb->setFont(fnt); rampupd = new QTimer(this); rampupd->setInterval(1000); + rampupd->stop(); QObject::connect(rampupd, &QTimer::timeout, this, &DeduperMainWindow::update_memusg); + this->setup_menu(); + this->update_actions(); sr = new SettingsRegistry(QStandardPaths::writableLocation(QStandardPaths::StandardLocation::ConfigLocation) + QString("/qdeduperrc")); int generalt = sr->register_tab("General"); @@ -173,6 +165,11 @@ DeduperMainWindow::DeduperMainWindow() std::string hkn = hkp.first.substr(3); sr->register_keyseq_option(hkt, "hotkey/" + hkn, QString(), hkp.second); } + for (int i = 0; i < ItemActionType::ACTION_MAX; ++i) + { + std::string iakt = "hotkey/item_action_mod_" + std::to_string(i); + sr->register_int_option(hkt, iakt, QString(), INT_MIN, INT_MAX, 0); + } prefdlg = new PreferenceDialog(sr, this); prefdlg->setModal(true); prefdlg->close(); @@ -181,32 +178,27 @@ DeduperMainWindow::DeduperMainWindow() QObject::connect(prefdlg, &PreferenceDialog::accepted, this, &DeduperMainWindow::apply_prefs); apply_prefs(); - for (size_t i = 0; i < keys.size(); ++i) + for (size_t i = 0; i < iadefkeys.size(); ++i) { - auto &k = keys[i]; - QAction *a = new QAction(); - a->setShortcut(QKeySequence(k)); - QObject::connect(a, &QAction::triggered, std::bind(&DeduperMainWindow::mark_toggle, this, i)); - selhk.push_back(a); + QAction *ma = new QAction(); + QObject::connect(ma, &QAction::triggered, std::bind(&DeduperMainWindow::mark_toggle, this, i)); + selhk.push_back(ma); + QAction *sa = new QAction(); - sa->setShortcut(QKeySequence(Qt::Modifier::SHIFT | k)); QObject::connect(sa, &QAction::triggered, std::bind(&DeduperMainWindow::mark_all_but, this, i)); selhk.push_back(sa); + QAction *ca = new QAction(); - ca->setShortcut(QKeySequence(Qt::Modifier::CTRL | k)); - QObject::connect(ca, &QAction::triggered, [this, i] { - if (i >= im->rowCount()) return; - if (id->is_single_item_mode()) - id->set_single_item_mode(false); - else - { - id->set_single_item_mode(true); - QTimer::singleShot(5, [this, i] { - lv->scrollTo(im->index(i, 0), QAbstractItemView::ScrollHint::PositionAtTop);}); - } - menuact["single_mode_toggle"]->setChecked(id->is_single_item_mode()); - }); + QObject::connect(ca, &QAction::triggered, std::bind(&DeduperMainWindow::show_only, this, i)); selhk.push_back(ca); + + QAction *oa = new QAction(); + QObject::connect(oa, &QAction::triggered, std::bind(&DeduperMainWindow::open_image, this, i)); + selhk.push_back(oa); + + QAction *la = new QAction(); + QObject::connect(la, &QAction::triggered, std::bind(&DeduperMainWindow::locate_image, this, i)); + selhk.push_back(la); } this->addActions(selhk); @@ -225,7 +217,7 @@ DeduperMainWindow::DeduperMainWindow() else cs = Qt::CheckState::Checked; this->im->setData(i, cs, Qt::ItemDataRole::CheckStateRole); this->marked_update(); - QDesktopServices::openUrl(QUrl::fromLocalFile(i.data(ImageItem::ImageItemRoles::path_role).toString())); + open_image(i.row()); }); l->addWidget(lv); l->addWidget(infopanel); @@ -271,7 +263,7 @@ void DeduperMainWindow::setup_menu() QPushButton *cancelbtn = pd->findChild(); if (Q_LIKELY(cancelbtn)) cancelbtn->setVisible(false); auto f = QtConcurrent::run([this, dbpath]() -> bool { - return this->sdb->load(qstring_to_path(dbpath)); + return this->sdb->load(utilities::qstring_to_path(dbpath)); }); QFutureWatcher *fw = new QFutureWatcher(this); fw->setFuture(f); @@ -298,7 +290,7 @@ void DeduperMainWindow::setup_menu() QObject::connect(save_db, &QAction::triggered, [this] { QString dbpath = QFileDialog::getSaveFileName(this, "Save Database", QString(), "Signature database (*.sigdb)"); if (!dbpath.isNull() && this->sdb) - this->sdb->save(qstring_to_path(dbpath)); + this->sdb->save(utilities::qstring_to_path(dbpath)); }); menuact["save_db"] = save_db; file->addSeparator(); @@ -316,7 +308,7 @@ void DeduperMainWindow::setup_menu() QObject::connect(search_img, &QAction::triggered, [this] { QString fpath = QFileDialog::getOpenFileName(this, "Select Image", QString(), "Image file (*.*)"); if (fpath.isNull()) return; - searched_image = qstring_to_path(fpath); + searched_image = utilities::qstring_to_path(fpath); search_image(searched_image); }); menuact["search_image"] = search_img; @@ -453,7 +445,7 @@ void DeduperMainWindow::setup_menu() QObject::connect(madir, &QAction::triggered, [this] { QString s = QFileDialog::getExistingDirectory(this, "Open"); if (s.isNull() || s.isEmpty()) return; - fs::path p = qstring_to_path(s); + fs::path p = utilities::qstring_to_path(s); for (auto &id : this->sdb->get_image_ids()) { fs::path fp = this->sdb->get_image_path(id); @@ -463,7 +455,7 @@ void DeduperMainWindow::setup_menu() for (int i = 0; i < im->rowCount(); ++i) { ImageItem *itm = static_cast(im->item(i)); - fs::path fp = qstring_to_path(itm->path()); + fs::path fp = utilities::qstring_to_path(itm->path()); itm->setCheckState(marked.find(fp) == marked.end() ? Qt::CheckState::Unchecked : Qt::CheckState::Checked); } }); @@ -472,17 +464,17 @@ void DeduperMainWindow::setup_menu() QObject::connect(madirr, &QAction::triggered, [this] { QString s = QFileDialog::getExistingDirectory(this, "Open"); if (s.isNull() || s.isEmpty()) return; - fs::path p = qstring_to_path(s); + fs::path p = utilities::qstring_to_path(s); for (auto &id : this->sdb->get_image_ids()) { fs::path fp = this->sdb->get_image_path(id); - if (!fsstr_to_qstring(fp.lexically_relative(p)).startsWith("../")) + if (!utilities::fspath_to_qstring(fp.lexically_relative(p)).startsWith("../")) this->marked.insert(fp); } for (int i = 0; i < im->rowCount(); ++i) { ImageItem *itm = static_cast(im->item(i)); - fs::path fp = qstring_to_path(itm->path()); + fs::path fp = utilities::qstring_to_path(itm->path()); itm->setCheckState(marked.find(fp) == marked.end() ? Qt::CheckState::Unchecked : Qt::CheckState::Checked); } }); @@ -499,6 +491,36 @@ void DeduperMainWindow::setup_menu() QAction *aboutqt = help->addAction("About Qt"); QObject::connect(aboutqt, &QAction::triggered, [this]{QMessageBox::aboutQt(this);}); + lv->setContextMenuPolicy(Qt::ContextMenuPolicy::CustomContextMenu); + QObject::connect(lv, &QWidget::customContextMenuRequested, [this] (const QPoint &pos) { + const QModelIndex &idx = lv->indexAt(pos); + if (!idx.isValid()) return; + Qt::CheckState cks = idx.data(Qt::ItemDataRole::CheckStateRole).value(); + QMenu *cm = new QMenu(this); + QAction *ma = cm->addAction(cks == Qt::CheckState::Checked ? "Unmark" : "Mark"); + QObject::connect(ma, &QAction::triggered, std::bind(&DeduperMainWindow::mark_toggle, this, idx.row())); + selhk.push_back(ma); + + QAction *sa = cm->addAction("Mark All Except"); + QObject::connect(sa, &QAction::triggered, std::bind(&DeduperMainWindow::mark_all_but, this, idx.row())); + selhk.push_back(sa); + + QAction *ca = cm->addAction(id->is_single_item_mode() ? "Restore" : "Maximize"); + QObject::connect(ca, &QAction::triggered, std::bind(&DeduperMainWindow::show_only, this, idx.row())); + selhk.push_back(ca); + + QAction *oa = cm->addAction("Open Image"); + QObject::connect(oa, &QAction::triggered, std::bind(&DeduperMainWindow::open_image, this, idx.row())); + selhk.push_back(oa); + + QAction *la = cm->addAction("Open Containing Folder"); + QObject::connect(la, &QAction::triggered, std::bind(&DeduperMainWindow::locate_image, this, idx.row())); + selhk.push_back(la); + + QObject::connect(cm, &QMenu::aboutToHide, [cm] {cm->deleteLater();}); + cm->popup(this->lv->mapToGlobal(pos)); + }); + tb = new QToolBar(this); this->addToolBar(tb); tb->addAction(prvgrp); @@ -571,7 +593,7 @@ void DeduperMainWindow::show_images(const std::vector &ids) for (auto &id : ids) fns.push_back(this->sdb->get_image_path(id)); fs::path::string_type common_pfx = common_prefix(fns); size_t idx = 0; - if (ids.size() > keys.size() && !nohotkeywarn && this->vm != ViewMode::view_marked) + if (ids.size() > iadefkeys.size() && !nohotkeywarn && this->vm != ViewMode::view_marked) nohotkeywarn = QMessageBox::StandardButton::Ignore == QMessageBox::warning(this, "Too many duplicates", @@ -581,9 +603,9 @@ void DeduperMainWindow::show_images(const std::vector &ids) for (auto &id : ids) { fs::path &f = fns[idx]; - ImageItem *imitm = new ImageItem(fsstr_to_qstring(f.native()), - fsstr_to_qstring(f.native().substr(common_pfx.length())), - idx < keys.size() ? keys[idx] : QKeySequence(), + ImageItem *imitm = new ImageItem(utilities::fspath_to_qstring(f), + utilities::fsstr_to_qstring(f.native().substr(common_pfx.length())), + idx < iadefkeys.size() ? iadefkeys[idx] : QKeySequence(), id, idx, lv->devicePixelRatioF()); imitm->setCheckState(marked.find(f) == marked.end() ? Qt::CheckState::Unchecked : Qt::CheckState::Checked); @@ -624,9 +646,9 @@ void DeduperMainWindow::save_list() { QString fn = QFileDialog::getSaveFileName(this, "Save list", QString(), "File List (*.txt)"); #if PATH_VALSIZE == 2 - std::wfstream fst(qstring_to_path(fn), std::ios_base::out); + std::wfstream fst(utilities::qstring_to_path(fn), std::ios_base::out); #else - std::fstream fst(qstring_to_path(fn), std::ios_base::out); + std::fstream fst(utilities::qstring_to_path(fn), std::ios_base::out); #endif if (fst.fail()) return; for (auto &x : this->marked) @@ -639,9 +661,9 @@ void DeduperMainWindow::load_list() { QString fn = QFileDialog::getOpenFileName(this, "Load list", QString(), "File List (*.txt)"); #if PATH_VALSIZE == 2 - std::wfstream fst(qstring_to_path(fn), std::ios_base::in); + std::wfstream fst(utilities::qstring_to_path(fn), std::ios_base::in); #else - std::fstream fst(qstring_to_path(fn), std::ios_base::in); + std::fstream fst(utilities::qstring_to_path(fn), std::ios_base::in); #endif if (fst.fail()) return; this->marked.clear(); @@ -655,7 +677,7 @@ void DeduperMainWindow::load_list() fst.close(); for (int i = 0; i < im->rowCount(); ++i) { - fs::path p = qstring_to_path(static_cast(im->item(i))->path()); + fs::path p = utilities::qstring_to_path(static_cast(im->item(i))->path()); im->item(i)->setCheckState(marked.find(p) != marked.end() ? Qt::CheckState::Checked : Qt::CheckState::Unchecked); } marked_update(); @@ -695,7 +717,7 @@ void DeduperMainWindow::scan_dirs(std::vector> paths) if (std::chrono::steady_clock::now() - lt > 100ms) { lt = std::chrono::steady_clock::now(); - QString etxt = this->fontMetrics().elidedText(QString("Looking for files to scan: %1").arg(fsstr_to_qstring(p)), + QString etxt = this->fontMetrics().elidedText(QString("Looking for files to scan: %1").arg(utilities::fspath_to_qstring(p)), Qt::TextElideMode::ElideMiddle, 475); this->pd->setLabelText(etxt); @@ -817,7 +839,7 @@ void DeduperMainWindow::sort_reassign_hotkeys() im->setSortRole(this->sort_role); im->sort(0, this->sort_order); for (int i = 0; i < im->rowCount(); ++i) - static_cast(im->item(i))->set_hotkey(i < keys.size() ? keys[i] : QKeySequence()); + static_cast(im->item(i))->set_hotkey(i < iadefkeys.size() ? iadefkeys[i] : QKeySequence()); } void DeduperMainWindow::mark_toggle(size_t x) @@ -885,7 +907,7 @@ void DeduperMainWindow::marked_update(bool update_msg) size_t m = 0; for (int i = 0; i < im->rowCount(); ++i) { - fs::path p = qstring_to_path(static_cast(im->item(i))->path()); + fs::path p = utilities::qstring_to_path(static_cast(im->item(i))->path()); if (im->item(i)->checkState() == Qt::CheckState::Checked) { marked.insert(p); @@ -899,6 +921,32 @@ void DeduperMainWindow::marked_update(bool update_msg) sb->showMessage(QString("%1 of %2 marked for deletion").arg(m).arg(im->rowCount()), 1000); } +void DeduperMainWindow::show_only(size_t x) +{ + if (x >= im->rowCount()) return; + if (id->is_single_item_mode()) + id->set_single_item_mode(false); + else + { + id->set_single_item_mode(true); + QTimer::singleShot(5, [this, x] { + lv->scrollTo(im->index(x, 0), QAbstractItemView::ScrollHint::PositionAtTop);}); + } + menuact["single_mode_toggle"]->setChecked(id->is_single_item_mode()); +} + +void DeduperMainWindow::open_image(size_t x) +{ + if (x >= im->rowCount()) return; + QDesktopServices::openUrl(QUrl::fromLocalFile(im->item(x, 0)->data(ImageItem::ImageItemRoles::path_role).toString())); +} + +void DeduperMainWindow::locate_image(size_t x) +{ + if (x >= im->rowCount()) return; + utilities::open_containing_folder(utilities::qstring_to_path(im->item(x, 0)->data(ImageItem::ImageItemRoles::path_role).toString())); +} + fs::path::string_type DeduperMainWindow::common_prefix(const std::vector &fns) { using fsstr = fs::path::string_type; diff --git a/qdeduper/mingui.hpp b/qdeduper/mingui.hpp index a4b2089..19db256 100644 --- a/qdeduper/mingui.hpp +++ b/qdeduper/mingui.hpp @@ -38,6 +38,16 @@ enum ViewMode view_marked }; +enum ItemActionType +{ + mark_toggle, + mark_all_except, + show_only, + open_with_system_viewer, + open_containing_folder, + ACTION_MAX +}; + class DeduperMainWindow : public QMainWindow { Q_OBJECT @@ -73,6 +83,9 @@ private: void mark_all(); void mark_none(bool msg = true); void marked_update(bool update_msg = true); + void show_only(size_t x); + void open_image(size_t x); + void locate_image(size_t x); fs::path::string_type common_prefix(const std::vector &fns); bool modified_check(bool quitting = true); protected: diff --git a/qdeduper/utilities.cpp b/qdeduper/utilities.cpp new file mode 100644 index 0000000..a2af953 --- /dev/null +++ b/qdeduper/utilities.cpp @@ -0,0 +1,65 @@ +#include "utilities.hpp" + +#include +#include +#include +#include +#ifdef HAS_QTDBUS +#include +#include +#endif + +namespace utilities +{ + +QString fspath_to_qstring(const fs::path &p) +{ + return fsstr_to_qstring(p.native()); +} + +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 +} + +fs::path qstring_to_path(const QString &s) +{ +#if PATH_VALSIZE == 2 //the degenerate platform + return fs::path(s.toStdWString()); +#else + return fs::path(s.toStdString()); +#endif +} + +void open_containing_folder(const fs::path &path) +{ +#ifdef _WIN32 + QProcess::startDetached("explorer", QStringList() << "/select," << QDir::toNativeSeparators(fileInfo.absoluteFilePath())); +#else +#ifdef HAS_QTDBUS + auto conn = QDBusConnection::sessionBus(); + auto call = QDBusMessage::createMethodCall( + "org.freedesktop.FileManager1", + "/org/freedesktop/FileManager1", + "org.freedesktop.FileManager1", + "ShowItems" + ); + QVariantList args = { + QStringList({fspath_to_qstring(path)}), + QString() + }; + call.setArguments(args); + auto resp = conn.call(call, QDBus::CallMode::Block, 500); + if (resp.type() != QDBusMessage::MessageType::ErrorMessage) + return; +#endif +#endif + auto par = (path / "../").lexically_normal(); + QDesktopServices::openUrl(QUrl::fromLocalFile(fspath_to_qstring(par))); +} + +}; diff --git a/qdeduper/utilities.hpp b/qdeduper/utilities.hpp new file mode 100644 index 0000000..a4f4f61 --- /dev/null +++ b/qdeduper/utilities.hpp @@ -0,0 +1,18 @@ +#ifndef UTILITIES_HPP +#define UTILITIES_HPP + +#include + +#include + +namespace fs = std::filesystem; + +namespace utilities +{ + void open_containing_folder(const fs::path &path); + QString fspath_to_qstring(const fs::path &p); + QString fsstr_to_qstring(const fs::path::string_type &s); + fs::path qstring_to_path(const QString &s); +}; + +#endif -- cgit v1.2.3