mirror of
https://github.com/subsurface/subsurface.git
synced 2025-01-19 06:15:26 +00:00
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>
This commit is contained in:
parent
f3ef38ca0d
commit
09fd5c40d1
9 changed files with 535 additions and 125 deletions
|
@ -1217,97 +1217,6 @@ QStringList imageExtensionFilters() {
|
|||
return filters;
|
||||
}
|
||||
|
||||
// Compare two full paths and return the number of matching levels, starting from the filename.
|
||||
// String comparison is case-insensitive.
|
||||
static int matchFilename(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;
|
||||
}
|
||||
|
||||
struct ImageMatch {
|
||||
QString localFilename;
|
||||
int score;
|
||||
};
|
||||
|
||||
static void learnImage(const QString &filename, QMap<QString, ImageMatch> &matches, const QVector<QString> &imageFilenames)
|
||||
{
|
||||
// Find the original filenames with the highest match-score
|
||||
QStringList newMatches;
|
||||
int bestScore = 1;
|
||||
|
||||
for (const QString &originalFilename: imageFilenames) {
|
||||
int score = matchFilename(filename, originalFilename);
|
||||
if (score < bestScore)
|
||||
continue;
|
||||
if (score > bestScore)
|
||||
newMatches.clear();
|
||||
newMatches.append(originalFilename);
|
||||
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 };
|
||||
}
|
||||
}
|
||||
|
||||
void learnImages(const QStringList &dirNames, int max_recursions, const QVector<QString> &imageFilenames)
|
||||
{
|
||||
QStringList filters = imageExtensionFilters();
|
||||
QMap<QString, ImageMatch> matches;
|
||||
|
||||
QVector<QStringList> stack; // Use a stack to recurse into directories
|
||||
stack.reserve(max_recursions + 1);
|
||||
stack.append(dirNames);
|
||||
while (!stack.isEmpty()) {
|
||||
if (stack.last().isEmpty()) {
|
||||
stack.removeLast();
|
||||
continue;
|
||||
}
|
||||
QDir dir(stack.last().takeLast());
|
||||
|
||||
for (const QString &file: dir.entryList(filters, QDir::Files))
|
||||
learnImage(dir.absoluteFilePath(file), matches, imageFilenames);
|
||||
if (stack.size() <= max_recursions) {
|
||||
stack.append(QStringList());
|
||||
for (const QString &dirname: dir.entryList(QStringList(), QDir::NoDotAndDotDot | QDir::Dirs))
|
||||
stack.last().append(dir.filePath(dirname));
|
||||
}
|
||||
}
|
||||
|
||||
for (auto it = matches.begin(); it != matches.end(); ++it)
|
||||
learnPictureFilename(it.key(), it->localFilename);
|
||||
|
||||
write_hashes();
|
||||
}
|
||||
|
||||
extern "C" const char *local_file_path(struct picture *picture)
|
||||
{
|
||||
return copy_qstring(localFilePath(picture->filename));
|
||||
|
|
|
@ -28,7 +28,6 @@ QString get_taglist_string(struct tag_entry *tag_list);
|
|||
void read_hashes();
|
||||
void write_hashes();
|
||||
QString thumbnailFileName(const QString &filename);
|
||||
void learnImages(const QStringList &dirNames, int max_recursions, const QVector<QString> &imageFilenames);
|
||||
void learnPictureFilename(const QString &originalName, const QString &localName);
|
||||
QString localFilePath(const QString &originalFilename);
|
||||
weight_t string_to_weight(const char *str);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
293
desktop-widgets/findmovedimagesdialog.cpp
Normal file
293
desktop-widgets/findmovedimagesdialog.cpp
Normal file
|
@ -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();
|
||||
}
|
49
desktop-widgets/findmovedimagesdialog.h
Normal file
49
desktop-widgets/findmovedimagesdialog.h
Normal file
|
@ -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
|
179
desktop-widgets/findmovedimagesdialog.ui
Normal file
179
desktop-widgets/findmovedimagesdialog.ui
Normal file
|
@ -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>
|
|
@ -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;
|
||||
|
|
|
@ -224,6 +224,7 @@ private:
|
|||
void configureToolbar();
|
||||
void setupSocialNetworkMenu();
|
||||
QDialog *survey;
|
||||
QDialog *findMovedImagesDialog;
|
||||
struct dive copyPasteDive;
|
||||
struct dive_components what;
|
||||
QList<QAction *> profileToolbarActions;
|
||||
|
|
Loading…
Add table
Reference in a new issue