aboutsummaryrefslogtreecommitdiff
path: root/qdeduper
diff options
context:
space:
mode:
authorGravatar Chris Xiong <chirs241097@gmail.com> 2022-09-30 23:58:46 -0400
committerGravatar Chris Xiong <chirs241097@gmail.com> 2022-09-30 23:59:21 -0400
commite25c84c0463f5a43d3b2bb836850f5c5963a2846 (patch)
tree504c65782410c5ead34b0941449a9abaa9c23001 /qdeduper
parentb6c8082dfc854b58cae798a6150681f7b9a343d3 (diff)
downloaddeduper-e25c84c0463f5a43d3b2bb836850f5c5963a2846.tar.xz
Add context menu for the image view.
Shortcuts are currently broken. Will be fixed in future commits.
Diffstat (limited to 'qdeduper')
-rw-r--r--qdeduper/CMakeLists.txt15
-rw-r--r--qdeduper/mingui.cpp180
-rw-r--r--qdeduper/mingui.hpp13
-rw-r--r--qdeduper/utilities.cpp65
-rw-r--r--qdeduper/utilities.hpp18
5 files changed, 223 insertions, 68 deletions
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