summaryrefslogtreecommitdiffstats
path: root/desktop-widgets
diff options
context:
space:
mode:
authorGravatar Berthold Stoeger <bstoeger@mail.tuwien.ac.at>2018-06-10 16:40:23 +0200
committerGravatar Dirk Hohndel <dirk@hohndel.org>2018-07-04 02:27:36 +0800
commit09fd5c40d1ad8a33efcfc829cf445ed1de9e4ff0 (patch)
tree28d216898ee6065f58a23b2a536f085104ddaa7c /desktop-widgets
parentf3ef38ca0d6db6f7e022a0e565f7460ed22f431e (diff)
downloadsubsurface-09fd5c40d1ad8a33efcfc829cf445ed1de9e4ff0.tar.gz
Dive pictures: implement FindMovedImagesDialog
Move the find-moved-images functions into a new translation unit and present the user with the identified matches before applying them. Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
Diffstat (limited to 'desktop-widgets')
-rw-r--r--desktop-widgets/CMakeLists.txt2
-rw-r--r--desktop-widgets/divelistview.h2
-rw-r--r--desktop-widgets/findmovedimagesdialog.cpp293
-rw-r--r--desktop-widgets/findmovedimagesdialog.h49
-rw-r--r--desktop-widgets/findmovedimagesdialog.ui179
-rw-r--r--desktop-widgets/mainwindow.cpp42
-rw-r--r--desktop-widgets/mainwindow.h1
7 files changed, 535 insertions, 33 deletions
diff --git a/desktop-widgets/CMakeLists.txt b/desktop-widgets/CMakeLists.txt
index b94b840d1..f5da2ae84 100644
--- a/desktop-widgets/CMakeLists.txt
+++ b/desktop-widgets/CMakeLists.txt
@@ -34,6 +34,7 @@ set (SUBSURFACE_UI
diveshareexportdialog.ui
downloadfromdivecomputer.ui
filterwidget.ui
+ findmovedimagesdialog.ui
listfilter.ui
locationInformation.ui
mainwindow.ui
@@ -66,6 +67,7 @@ set(SUBSURFACE_INTERFACE
diveplanner.cpp
diveshareexportdialog.cpp
downloadfromdivecomputer.cpp
+ findmovedimagesdialog.cpp
kmessagewidget.cpp
mainwindow.cpp
mapwidget.cpp
diff --git a/desktop-widgets/divelistview.h b/desktop-widgets/divelistview.h
index 47b9b5aeb..bd339fa00 100644
--- a/desktop-widgets/divelistview.h
+++ b/desktop-widgets/divelistview.h
@@ -36,6 +36,7 @@ public:
void contextMenuEvent(QContextMenuEvent *event);
QList<dive_trip_t *> selectedTrips();
static QString lastUsedImageDir();
+ static void updateLastUsedImageDir(const QString &s);
public
slots:
void toggleColumnVisibilityByIndex();
@@ -80,7 +81,6 @@ private:
void restoreExpandedRows();
int lastVisibleColumn();
void selectTrip(dive_trip_t *trip);
- void updateLastUsedImageDir(const QString &s);
void updateLastImageTimeOffset(int offset);
int lastImageTimeOffset();
void addToTrip(int delta);
diff --git a/desktop-widgets/findmovedimagesdialog.cpp b/desktop-widgets/findmovedimagesdialog.cpp
new file mode 100644
index 000000000..18a9565b1
--- /dev/null
+++ b/desktop-widgets/findmovedimagesdialog.cpp
@@ -0,0 +1,293 @@
+// SPDX-License-Identifier: GPL-2.0
+#include "findmovedimagesdialog.h"
+#include "core/qthelper.h"
+#include "desktop-widgets/divelistview.h" // TODO: used for lastUsedImageDir()
+#include "qt-models/divepicturemodel.h"
+
+#include <QFileDialog>
+#include <QtConcurrent>
+
+FindMovedImagesDialog::FindMovedImagesDialog(QWidget *parent) : QDialog(parent)
+{
+ ui.setupUi(this);
+ fontMetrics.reset(new QFontMetrics(ui.scanning->font()));
+ connect(&watcher, &QFutureWatcher<QVector<Match>>::finished, this, &FindMovedImagesDialog::searchDone);
+ connect(ui.buttonBox->button(QDialogButtonBox::Apply), &QAbstractButton::clicked, this, &FindMovedImagesDialog::apply);
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(false);
+}
+
+// Compare two full paths and return the number of matching levels, starting from the filename.
+// String comparison is case-insensitive.
+static int matchPath(const QString &path1, const QString &path2)
+{
+ QFileInfo f1(path1);
+ QFileInfo f2(path2);
+
+ int score = 0;
+ for (;;) {
+ QString fn1 = f1.fileName();
+ QString fn2 = f2.fileName();
+ if (fn1.isEmpty() || fn2.isEmpty())
+ break;
+ if (fn1 == ".") {
+ f1 = QFileInfo(f1.path());
+ continue;
+ }
+ if (fn2 == ".") {
+ f2 = QFileInfo(f2.path());
+ continue;
+ }
+ if (QString::compare(fn1, fn2, Qt::CaseInsensitive) != 0)
+ break;
+ f1 = QFileInfo(f1.path());
+ f2 = QFileInfo(f2.path());
+ ++score;
+ }
+ return score;
+}
+
+FindMovedImagesDialog::ImagePath::ImagePath(const QString &path) : fullPath(path),
+ filenameUpperCase(QFileInfo(path).fileName().toUpper())
+{
+}
+
+bool FindMovedImagesDialog::ImagePath::operator<(const ImagePath &path2) const
+{
+ return filenameUpperCase < path2.filenameUpperCase;
+}
+
+void FindMovedImagesDialog::learnImage(const QString &filename, QMap<QString, ImageMatch> &matches, const QVector<ImagePath> &imagePaths)
+{
+ QStringList newMatches;
+ int bestScore = 1;
+ // Find matching file paths by a binary search of the file name
+ ImagePath path(filename);
+ for (auto it = std::lower_bound(imagePaths.begin(), imagePaths.end(), path);
+ it != imagePaths.end() && it->filenameUpperCase == path.filenameUpperCase;
+ ++it) {
+ int score = matchPath(filename, it->fullPath);
+ if (score < bestScore)
+ continue;
+ if (score > bestScore)
+ newMatches.clear();
+ newMatches.append(it->fullPath);
+ bestScore = score;
+ }
+
+ // Add the new original filenames to the list of matches, if the score is higher than previously
+ for (const QString &originalFilename: newMatches) {
+ auto it = matches.find(originalFilename);
+ if (it == matches.end())
+ matches.insert(originalFilename, { filename, bestScore });
+ else if (it->score < bestScore)
+ *it = { filename, bestScore };
+ }
+}
+
+// We use a stack to recurse into directories. Each level of the stack is made up of
+// a list of subdirectories to process. For each directory we keep track of the progress
+// that is done when processing this directory. In principle the from value is redundant
+// (it could be extracted from previous stack entries, but it makes code simpler.
+struct Dir {
+ QString path;
+ double progressFrom, progressTo;
+};
+
+QVector<FindMovedImagesDialog::Match> FindMovedImagesDialog::learnImages(const QString &dir, int maxRecursions, QVector<QString> imagePathsIn)
+{
+ QMap<QString, ImageMatch> matches;
+
+ // For divelogs with thousands of images, we don't want to compare the path of every image.
+ // Therefore, keep an array of image paths sorted by the filename in upper case.
+ // Thus, we can access all paths ending in the same filename by a binary search. We suppose that
+ // there aren't many pictures with the same filename but different paths.
+ QVector<ImagePath> imagePaths;
+ imagePaths.reserve(imagePathsIn.size());
+ for (const QString &path: imagePathsIn)
+ imagePaths.append(ImagePath(path)); // No emplace() in QVector? Sheesh.
+ std::sort(imagePaths.begin(), imagePaths.end());
+
+ // Free memory of original path vector - we don't need it any more
+ imagePathsIn.clear();
+
+ QVector<QVector<Dir>> stack; // Use a stack to recurse into directories
+ stack.reserve(maxRecursions + 1);
+ stack.append({ { dir, 0.0, 1.0 } });
+ while (!stack.isEmpty()) {
+ if (stack.last().isEmpty()) {
+ stack.removeLast();
+ continue;
+ }
+ Dir entry = stack.last().takeLast();
+ QDir dir(entry.path);
+
+ // Since we're running in a different thread, use invokeMethod to set progress.
+ QMetaObject::invokeMethod(this, "setProgress", Q_ARG(double, entry.progressFrom), Q_ARG(QString, dir.absolutePath()));
+
+ for (const QString &file: dir.entryList(QDir::Files)) {
+ if (stopScanning != 0)
+ goto out;
+ learnImage(dir.absoluteFilePath(file), matches, imagePaths);
+ }
+ if (stack.size() <= maxRecursions) {
+ stack.append(QVector<Dir>());
+ QVector<Dir> &newItem = stack.last();
+ for (const QString &dirname: dir.entryList(QDir::NoDotAndDotDot | QDir::Dirs))
+ stack.last().append({ dir.filePath(dirname), 0.0, 0.0 });
+ int num = newItem.size();
+ double diff = entry.progressTo - entry.progressFrom;
+ // We pop from back therefore we fill the progress in reverse
+ for (int i = 0; i < num; ++i) {
+ newItem[num - i - 1].progressFrom = (i / (double)num) * diff + entry.progressFrom;
+ newItem[num - i - 1].progressTo = ((i + 1) / (double)num) * diff + entry.progressFrom;
+ }
+ }
+ }
+out:
+ QMetaObject::invokeMethod(this, "setProgress", Q_ARG(double, 1.0), Q_ARG(QString, QString()));
+ QVector<FindMovedImagesDialog::Match> ret;
+ for (auto it = matches.begin(); it != matches.end(); ++it)
+ ret.append({ it.key(), it->localFilename, it->score });
+ return ret;
+}
+
+void FindMovedImagesDialog::setProgress(double progress, QString path)
+{
+ ui.progress->setValue((int)(progress * 100.0));
+
+ // Elide text to avoid rescaling of the window if path is too long.
+ // Note that we subtract an arbitrary 10 pixels from the width, because otherwise the label slowly grows.
+ QString elidedPath = fontMetrics->elidedText(path, Qt::ElideMiddle, ui.scanning->width() - 10);
+ ui.scanning->setText(elidedPath);
+}
+
+void FindMovedImagesDialog::on_scanButton_clicked()
+{
+ if (watcher.isRunning()) {
+ stopScanning = 1;
+ return;
+ }
+
+ // TODO: is lastUsedImageDir really well-placed in DiveListView?
+ QString dirName = QFileDialog::getExistingDirectory(this,
+ tr("Traverse image directories"),
+ DiveListView::lastUsedImageDir(),
+ QFileDialog::ShowDirsOnly);
+ if (dirName.isEmpty())
+ return;
+ DiveListView::updateLastUsedImageDir(dirName);
+ ui.scanButton->setText(tr("Stop scanning"));
+ ui.buttonBox->setEnabled(false);
+ ui.imagesText->clear();
+ // We have to collect the names of the image filenames in the main thread
+ bool onlySelected = ui.onlySelectedDives->isChecked();
+ QVector<QString> imagePaths;
+ int i;
+ struct dive *dive;
+ for_each_dive (i, dive)
+ if (!onlySelected || dive->selected)
+ FOR_EACH_PICTURE(dive)
+ imagePaths.append(QString(picture->filename));
+ stopScanning = 0;
+ QFuture<QVector<Match>> future = QtConcurrent::run(
+ // Note that we capture everything but "this" by copy to avoid dangling references.
+ [this, dirName, imagePaths]()
+ { return learnImages(dirName, 20, imagePaths);}
+ );
+ watcher.setFuture(future);
+}
+
+static QString formatPath(const QString &path, int numBold)
+{
+ QString res;
+ QVector<QString> boldPaths;
+ boldPaths.reserve(numBold);
+ QFileInfo info(path);
+ for (int i = 0; i < numBold; ++i) {
+ QString fn = info.fileName();
+ if (fn.isEmpty())
+ break;
+ boldPaths.append(fn);
+ info = QFileInfo(info.path());
+ }
+ QString nonBoldPath = info.filePath();
+ QString separator = QDir::separator();
+ if (!nonBoldPath.isEmpty()) {
+ res += nonBoldPath.toHtmlEscaped();
+ if (!boldPaths.isEmpty() && nonBoldPath[nonBoldPath.size() - 1] != QDir::separator())
+ res += separator;
+ }
+
+ if (boldPaths.size() > 0) {
+ res += "<b>";
+ for (int i = boldPaths.size() - 1; i >= 0; --i) {
+ res += boldPaths[i].toHtmlEscaped();
+ if (i > 0)
+ res += separator;
+ }
+ res += "</b>";
+ }
+ return res;
+}
+
+static bool sameFile(const QString &f1, const QString &f2)
+{
+ return QFileInfo(f1) == QFileInfo(f2);
+}
+
+void FindMovedImagesDialog::searchDone()
+{
+ ui.scanButton->setText(tr("Select folder and scan"));
+ ui.buttonBox->setEnabled(true);
+ ui.scanning->clear();
+
+ matches = watcher.result();
+ ui.imagesText->clear();
+
+ QString text;
+ int numChanged = 0;
+ if (stopScanning != 0) {
+ text += "<b>" + tr("Scanning cancelled - results may be incomplete") + "</b><br/>";
+ stopScanning = 0;
+ }
+ if (matches.isEmpty()) {
+ text += "<i>" + tr("No matching images found") + "</i>";
+ } else {
+ QString matchesText;
+ for (const Match &match: matches) {
+ if (!sameFile(match.localFilename, localFilePath(match.originalFilename))) {
+ ++numChanged;
+ matchesText += formatPath(match.originalFilename, match.matchingPathItems) + " → " +
+ formatPath(match.localFilename, match.matchingPathItems) + "<br/>";
+ }
+ }
+ int numUnchanged = matches.size() - numChanged;
+ if (numUnchanged > 0)
+ text += tr("Found <b>%1</b> images at their current place.").arg(numUnchanged) + "<br/>";
+ if (numChanged > 0) {
+ text += tr("Found <b>%1</b> images at new locations:").arg(numChanged) + "<br/>";
+ text += matchesText;
+ }
+ }
+ ui.imagesText->setHtml(text);
+ ui.buttonBox->button(QDialogButtonBox::Apply)->setEnabled(numChanged > 0);
+}
+
+void FindMovedImagesDialog::apply()
+{
+ for (const Match &match: matches)
+ learnPictureFilename(match.originalFilename, match.localFilename);
+ write_hashes();
+ DivePictureModel::instance()->updateDivePictures();
+
+ ui.imagesText->clear();
+ matches.clear();
+ hide();
+ close();
+}
+
+void FindMovedImagesDialog::on_buttonBox_rejected()
+{
+ ui.imagesText->clear();
+ matches.clear();
+}
diff --git a/desktop-widgets/findmovedimagesdialog.h b/desktop-widgets/findmovedimagesdialog.h
new file mode 100644
index 000000000..9cae6e8fd
--- /dev/null
+++ b/desktop-widgets/findmovedimagesdialog.h
@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: GPL-2.0
+#ifndef FINDMOVEDIMAGES_H
+#define FINDMOVEDIMAGES_H
+
+#include "ui_findmovedimagesdialog.h"
+#include <QFutureWatcher>
+#include <QVector>
+#include <QMap>
+#include <QAtomicInteger>
+
+class FindMovedImagesDialog : public QDialog {
+ Q_OBJECT
+public:
+ FindMovedImagesDialog(QWidget *parent = 0);
+private
+slots:
+ void on_scanButton_clicked();
+ void apply();
+ void on_buttonBox_rejected();
+ void setProgress(double progress, QString path);
+ void searchDone();
+private:
+ struct Match {
+ QString originalFilename;
+ QString localFilename;
+ int matchingPathItems;
+ };
+ struct ImageMatch {
+ QString localFilename;
+ int score;
+ };
+ struct ImagePath {
+ QString fullPath;
+ QString filenameUpperCase;
+ ImagePath() = default; // For some reason QVector<...>::reserve() needs a default constructor!?
+ ImagePath(const QString &path);
+ inline bool operator<(const ImagePath &path2) const;
+ };
+ Ui::FindMovedImagesDialog ui;
+ QFutureWatcher<QVector<Match>> watcher;
+ QVector<Match> matches;
+ QAtomicInt stopScanning;
+ QScopedPointer<QFontMetrics> fontMetrics; // Needed to format elided paths
+
+ void learnImage(const QString &filename, QMap<QString, ImageMatch> &matches, const QVector<ImagePath> &imagePaths);
+ QVector<Match> learnImages(const QString &dir, int maxRecursions, QVector<QString> imagePaths);
+};
+
+#endif
diff --git a/desktop-widgets/findmovedimagesdialog.ui b/desktop-widgets/findmovedimagesdialog.ui
new file mode 100644
index 000000000..b558feec8
--- /dev/null
+++ b/desktop-widgets/findmovedimagesdialog.ui
@@ -0,0 +1,179 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>FindMovedImagesDialog</class>
+ <widget class="QDialog" name="FindMovedImagesDialog">
+ <property name="windowModality">
+ <enum>Qt::WindowModal</enum>
+ </property>
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>693</width>
+ <height>620</height>
+ </rect>
+ </property>
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Minimum">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="windowTitle">
+ <string>Find moved images</string>
+ </property>
+ <property name="windowIcon">
+ <iconset>
+ <normalon>:subsurface-icon</normalon>
+ </iconset>
+ </property>
+ <property name="modal">
+ <bool>false</bool>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_2">
+ <item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>Found images</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <item>
+ <widget class="QTextEdit" name="imagesText">
+ <property name="lineWrapMode">
+ <enum>QTextEdit::NoWrap</enum>
+ </property>
+ <property name="readOnly">
+ <bool>true</bool>
+ </property>
+ <property name="text" stdset="0">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="progress">
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QLabel" name="onlySelectedDiveslabel">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Match only images in selected dive(s)</string>
+ </property>
+ <property name="alignment">
+ <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="onlySelectedDives">
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout"/>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="QLabel" name="scanningLabel">
+ <property name="text">
+ <string>Scanning:</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="scanning">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="font">
+ <font>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="scanButton">
+ <property name="text">
+ <string>Select folder and scan</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QDialogButtonBox" name="buttonBox">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="standardButtons">
+ <set>QDialogButtonBox::Apply|QDialogButtonBox::Cancel</set>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources>
+ <include location="../subsurface.qrc"/>
+ </resources>
+ <connections>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>accepted()</signal>
+ <receiver>FindMovedImagesDialog</receiver>
+ <slot>accept()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>248</x>
+ <y>254</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>157</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ <connection>
+ <sender>buttonBox</sender>
+ <signal>rejected()</signal>
+ <receiver>FindMovedImagesDialog</receiver>
+ <slot>reject()</slot>
+ <hints>
+ <hint type="sourcelabel">
+ <x>316</x>
+ <y>260</y>
+ </hint>
+ <hint type="destinationlabel">
+ <x>286</x>
+ <y>274</y>
+ </hint>
+ </hints>
+ </connection>
+ </connections>
+</ui>
diff --git a/desktop-widgets/mainwindow.cpp b/desktop-widgets/mainwindow.cpp
index dcd7aff32..a7cf6d82e 100644
--- a/desktop-widgets/mainwindow.cpp
+++ b/desktop-widgets/mainwindow.cpp
@@ -42,6 +42,7 @@
#include "desktop-widgets/divelogimportdialog.h"
#include "desktop-widgets/divelogexportdialog.h"
#include "desktop-widgets/usersurvey.h"
+#include "desktop-widgets/findmovedimagesdialog.h"
#include "core/divesitehelpers.h"
#include "core/windowtitleupdate.h"
#include "desktop-widgets/locationinformation.h"
@@ -108,7 +109,8 @@ MainWindow::MainWindow() : QMainWindow(),
helpView(0),
#endif
state(VIEWALL),
- survey(0)
+ survey(0),
+ findMovedImagesDialog(0)
{
Q_ASSERT_X(m_Instance == NULL, "MainWindow", "MainWindow recreated!");
m_Instance = this;
@@ -701,37 +703,6 @@ void MainWindow::on_actionCloudOnline_triggered()
updateCloudOnlineStatus();
}
-static void learnImageDirs(QStringList dirnames, QVector<QString> imageFilenames)
-{
- learnImages(dirnames, 10, imageFilenames);
- DivePictureModel::instance()->updateDivePictures();
-}
-
-void MainWindow::on_actionHash_images_triggered()
-{
- QFuture<void> future;
- QFileDialog dialog(this, tr("Traverse image directories"), lastUsedDir());
- dialog.setFileMode(QFileDialog::Directory);
- dialog.setViewMode(QFileDialog::Detail);
- dialog.setLabelText(QFileDialog::Accept, tr("Scan"));
- dialog.setLabelText(QFileDialog::Reject, tr("Cancel"));
- QStringList dirnames;
- if (dialog.exec())
- dirnames = dialog.selectedFiles();
- if (dirnames.isEmpty())
- return;
- QVector<QString> imageFilenames;
- int i;
- struct dive *dive;
- for_each_dive (i, dive)
- FOR_EACH_PICTURE(dive)
- imageFilenames.append(QString(picture->filename));
- future = QtConcurrent::run(learnImageDirs, dirnames, imageFilenames);
- MainWindow::instance()->getNotificationWidget()->showNotification(tr("Scanning images...(this can take a while)"), KMessageWidget::Information);
- MainWindow::instance()->getNotificationWidget()->setFuture(future);
-
-}
-
ProfileWidget2 *MainWindow::graphics() const
{
return qobject_cast<ProfileWidget2*>(applicationState["Default"].topRight->layout()->itemAt(1)->widget());
@@ -1375,6 +1346,13 @@ void MainWindow::on_actionUserSurvey_triggered()
survey->show();
}
+void MainWindow::on_actionHash_images_triggered()
+{
+ if(!findMovedImagesDialog)
+ findMovedImagesDialog = new FindMovedImagesDialog(this);
+ findMovedImagesDialog->show();
+}
+
QString MainWindow::filter_open()
{
QString f;
diff --git a/desktop-widgets/mainwindow.h b/desktop-widgets/mainwindow.h
index 6c2b7c1f4..ab9ebf316 100644
--- a/desktop-widgets/mainwindow.h
+++ b/desktop-widgets/mainwindow.h
@@ -224,6 +224,7 @@ private:
void configureToolbar();
void setupSocialNetworkMenu();
QDialog *survey;
+ QDialog *findMovedImagesDialog;
struct dive copyPasteDive;
struct dive_components what;
QList<QAction *> profileToolbarActions;