subsurface/desktop-widgets/findmovedimagesdialog.cpp
Berthold Stoeger 8a3a0edb83 cleanup: silence std::move()-related Coverity warnings
Unfortunately Coverity doesn't understand that most Qt data
structures are copy-on-write. It's a mis-feature of Qt, but
it is the way it is. Thus, passing by value is not an issue.

Out of ca. 25 warnings only two were legit. Let's silence
the others by either std::move()ing or passing by reference,
as would be idiomatic C++, which Qt is not.

Signed-off-by: Berthold Stoeger <bstoeger@mail.tuwien.ac.at>
2024-01-15 15:11:36 -08:00

295 lines
9.5 KiB
C++

// SPDX-License-Identifier: GPL-2.0
#include "findmovedimagesdialog.h"
#include "core/picture.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 &rootdir, 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({ { rootdir, 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 media 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, std::move(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 media files 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> media files at their current place.").arg(numUnchanged) + "<br/>";
if (numChanged > 0) {
text += tr("Found <b>%1</b> media files 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();
}