From e25c84c0463f5a43d3b2bb836850f5c5963a2846 Mon Sep 17 00:00:00 2001
From: Chris Xiong <chirs241097@gmail.com>
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 <climits>
 #include <chrono>
 #include <fstream>
 #include <thread>
@@ -48,7 +50,7 @@
 #include <QInputDialog>
 #include <QDesktopServices>
 
-const std::vector<int> keys = {
+const std::vector<int> 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<std::string, QKeySequence> 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<int> 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<fs::path>();
     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<QPushButton*>();
             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<bool> *fw = new QFutureWatcher<bool>(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<ImageItem*>(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<ImageItem*>(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<Qt::CheckState>();
+        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<size_t> &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<size_t> &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<ImageItem*>(im->item(i))->path());
+        fs::path p = utilities::qstring_to_path(static_cast<ImageItem*>(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<std::pair<fs::path, bool>> 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<ImageItem*>(im->item(i))->set_hotkey(i < keys.size() ? keys[i] : QKeySequence());
+        static_cast<ImageItem*>(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<ImageItem*>(im->item(i))->path());
+        fs::path p = utilities::qstring_to_path(static_cast<ImageItem*>(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<fs::path> &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<fs::path> &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 <QDesktopServices>
+#include <QProcess>
+#include <QUrl>
+#include <QDebug>
+#ifdef HAS_QTDBUS
+#include <QDBusConnection>
+#include <QDBusMessage>
+#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 <filesystem>
+
+#include <QString>
+
+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