File indexing completed on 2024-05-19 04:56:01

0001 /**
0002  * \file dirrenamer.cpp
0003  * Directory renamer.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 23 Jul 2011
0008  *
0009  * Copyright (C) 2011-2024  Urs Fleisch
0010  *
0011  * This file is part of Kid3.
0012  *
0013  * Kid3 is free software; you can redistribute it and/or modify
0014  * it under the terms of the GNU General Public License as published by
0015  * the Free Software Foundation; either version 2 of the License, or
0016  * (at your option) any later version.
0017  *
0018  * Kid3 is distributed in the hope that it will be useful,
0019  * but WITHOUT ANY WARRANTY; without even the implied warranty of
0020  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0021  * GNU General Public License for more details.
0022  *
0023  * You should have received a copy of the GNU General Public License
0024  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
0025  */
0026 
0027 #include "dirrenamer.h"
0028 #include <QFileInfo>
0029 #include <QDir>
0030 #include <QCoreApplication>
0031 #include "trackdata.h"
0032 #include "saferename.h"
0033 #include "taggedfilesystemmodel.h"
0034 #include "modeliterator.h"
0035 #include "formatconfig.h"
0036 
0037 /**
0038  * Data collected by DirNameFormatReplacer during a rename session.
0039  */
0040 class DirNameFormatReplacerContext {
0041 public:
0042   /**
0043    * Store value for aggregate function.
0044    * @param code aggregating code, e.g. "max-year"
0045    * @param value value of base code (e.g. "year")
0046    */
0047   void addValue(const QString& code, const QString& value);
0048 
0049   /**
0050    * Register the replaced directory name which still contains
0051    * placeholders for the aggregate codes.
0052    * @param dirName directory name with replacements and aggregate codes,
0053    *                QString() to terminate the rename session
0054    */
0055   void putDirName(const QString& dirName);
0056 
0057   /**
0058    * Get and clear the replacements for all the replacement codes
0059    * encountered during this rename session.
0060    * Shall be called at the end of the rename session
0061    * @return list of (directory with aggregate codes,
0062    *         directory with replaced aggregate codes) pairs.
0063    */
0064   QList<QPair<QString, QString>> takeReplacements();
0065 
0066   /**
0067    * Check if aggregated codes are used.
0068    * @return true if aggregated codes have been added using addValue().
0069    */
0070   bool hasAggregatedCodes() const { return !m_aggregatedCodes.isEmpty(); }
0071 
0072 private:
0073   QString getAggregate(const QString& code) const;
0074 
0075   QList<QPair<QString, QString>> m_replacements;
0076   QHash<QString, QStringList> m_currentCodes;
0077   QHash<QString, QStringList> m_aggregatedCodes;
0078   QString m_aggregatedDirName;
0079 };
0080 
0081 void DirNameFormatReplacerContext::addValue(const QString& code,
0082                                             const QString& value)
0083 {
0084   m_currentCodes[code].append(value);
0085 }
0086 
0087 void DirNameFormatReplacerContext::putDirName(const QString& dirName)
0088 {
0089   if (m_aggregatedDirName.isEmpty()) {
0090     // First directory name, start aggregation.
0091     m_aggregatedDirName = dirName;
0092     m_aggregatedCodes = m_currentCodes;
0093   } else if (m_aggregatedDirName != dirName) {
0094     // New directory name, replace aggregated values and return result.
0095     QString replacedDirName = m_aggregatedDirName;
0096     for (auto it = m_aggregatedCodes.constBegin();
0097          it != m_aggregatedCodes.constEnd();
0098          ++it) {
0099       replacedDirName.replace(it.key(), getAggregate(it.key()));
0100     }
0101     if (replacedDirName != m_aggregatedDirName) {
0102       m_replacements.append({m_aggregatedDirName, replacedDirName});
0103     }
0104     m_aggregatedCodes = m_currentCodes;
0105     m_aggregatedDirName = dirName;
0106   } else {
0107     // Still the same directory name, keep on aggregating.
0108     for (auto it = m_currentCodes.constBegin();
0109          it != m_currentCodes.constEnd();
0110          ++it) {
0111       m_aggregatedCodes[it.key()].append(it.value());
0112     }
0113   }
0114   m_currentCodes.clear();
0115 }
0116 
0117 QList<QPair<QString, QString>> DirNameFormatReplacerContext::takeReplacements()
0118 {
0119   // Terminate current directory aggregation.
0120   putDirName(QString());
0121   QList<QPair<QString, QString>> result;
0122   m_replacements.swap(result);
0123   return result;
0124 }
0125 
0126 QString DirNameFormatReplacerContext::getAggregate(const QString& code) const
0127 {
0128   QString result;
0129   const QStringList values = m_aggregatedCodes.value(code);
0130   if (code.startsWith(QLatin1String("max-"))) {
0131     for (const QString& value : values) {
0132       if (value > result) {
0133         result = value;
0134       }
0135     }
0136   } else if (code.startsWith(QLatin1String("min-"))) {
0137     for (const QString& value : values) {
0138       if (result.isNull() || value < result) {
0139         result = value;
0140       }
0141     }
0142   } else if (code.startsWith(QLatin1String("unq-"))) {
0143     for (const QString& value : values) {
0144       if (result.isNull()) {
0145         result = value;
0146       } else if (value != result) {
0147         result.clear();
0148         break;
0149       }
0150     }
0151   }
0152   return result;
0153 }
0154 
0155 
0156 namespace {
0157 
0158 /**
0159  * Specialized track data format replacer using context to support
0160  * aggregate functions.
0161  */
0162 class DirNameFormatReplacer : public TrackDataFormatReplacer {
0163 public:
0164   /**
0165    * Constructor.
0166    * @param context context to store aggregate data
0167    * @param trackData track data
0168    * @param str string with format codes
0169    */
0170   explicit DirNameFormatReplacer(
0171     DirNameFormatReplacerContext& context,
0172     const TrackData& trackData,
0173     const QString& str = QString());
0174 
0175   ~DirNameFormatReplacer() override = default;
0176 
0177   DirNameFormatReplacer(const DirNameFormatReplacer& other) = delete;
0178   DirNameFormatReplacer &operator=(const DirNameFormatReplacer& other) = delete;
0179 
0180 protected:
0181   QString getReplacement(const QString& code) const override;
0182 
0183 private:
0184   DirNameFormatReplacerContext& m_context;
0185 };
0186 
0187 DirNameFormatReplacer::DirNameFormatReplacer(
0188     DirNameFormatReplacerContext& context,
0189     const TrackData& trackData,
0190     const QString& str)
0191   : TrackDataFormatReplacer(trackData, str), m_context(context)
0192 {
0193 }
0194 
0195 QString DirNameFormatReplacer::getReplacement(const QString& code) const
0196 {
0197   if (code.startsWith(QLatin1String("max-")) ||
0198       code.startsWith(QLatin1String("min-")) ||
0199       code.startsWith(QLatin1String("unq-"))) {
0200     QString value = TrackDataFormatReplacer::getReplacement(code.mid(4));
0201     m_context.addValue(code, value);
0202     return code;
0203   }
0204   return TrackDataFormatReplacer::getReplacement(code);
0205 }
0206 
0207 
0208 /**
0209  * Get parent directory.
0210  *
0211  * @param dir directory
0212  *
0213  * @return parent directory (terminated by separator),
0214  *         empty string if no separator in dir.
0215  */
0216 QString parentDirectory(const QString& dir)
0217 {
0218   QString parent(dir);
0219   if (int slashPos = parent.lastIndexOf(QLatin1Char('/')); slashPos != -1) {
0220     parent.truncate(slashPos + 1);
0221   } else {
0222     parent = QLatin1String("");
0223   }
0224   return parent;
0225 }
0226 
0227 }
0228 
0229 /**
0230  * Constructor.
0231  * @param parent parent object
0232  */
0233 DirRenamer::DirRenamer(QObject* parent) : QObject(parent),
0234   m_fmtContext(new DirNameFormatReplacerContext),
0235   m_tagVersion(Frame::TagVAll), m_aborted(false), m_actionCreate(false)
0236 {
0237   setObjectName(QLatin1String("DirRenamer"));
0238 }
0239 
0240 /**
0241  * Destructor.
0242  */
0243 DirRenamer::~DirRenamer()
0244 {
0245   delete m_fmtContext;
0246 }
0247 
0248 /** Only defined for generation of translation files */
0249 #define CREATE_DIR_FAILED_FOR_PO QT_TRANSLATE_NOOP("@default", "Create folder %1 failed\n")
0250 
0251 /**
0252  * Create a directory if it does not exist.
0253  *
0254  * @param dir      directory path
0255  * @param index    model index of item to rename
0256  * @param errorMsg if not NULL and an error occurred, a message is appended here,
0257  *                 otherwise it is not touched
0258  *
0259  * @return true if directory exists or was created successfully.
0260  */
0261 bool DirRenamer::createDirectory(
0262     const QString& dir, const QPersistentModelIndex& index,
0263     QString* errorMsg) const
0264 {
0265   if (auto model = const_cast<TaggedFileSystemModel*>(
0266         qobject_cast<const TaggedFileSystemModel*>(index.model()))) {
0267     const QString parentDirName = model->filePath(index.parent());
0268     if (const QString relativeName = QDir(parentDirName).relativeFilePath(dir);
0269         model->mkdir(index.parent(), relativeName).isValid() &&
0270         QFileInfo(dir).isDir()) {
0271       return true;
0272     }
0273   }
0274   if (QFileInfo(dir).isDir() ||
0275     (QDir().mkdir(dir) && QFileInfo(dir).isDir())) {
0276     return true;
0277   }
0278   if (errorMsg) {
0279     errorMsg->append(tr("Create folder %1 failed\n").arg(dir));
0280   }
0281   return false;
0282 }
0283 
0284 /** Only defined for generation of translation files */
0285 #define FILE_ALREADY_EXISTS_FOR_PO QT_TRANSLATE_NOOP("@default", "File %1 already exists\n")
0286 /** Only defined for generation of translation files */
0287 #define IS_NOT_DIR_FOR_PO QT_TRANSLATE_NOOP("@default", "%1 is not a folder\n")
0288 /** Only defined for generation of translation files */
0289 #define RENAME_FAILED_FOR_PO QT_TRANSLATE_NOOP("@default", "Rename %1 to %2 failed\n")
0290 
0291 /**
0292  * Rename a directory.
0293  *
0294  * @param olddir   old directory name
0295  * @param newdir   new directory name
0296  * @param index    model index of item to rename
0297  * @param errorMsg if not NULL and an error occurred, a message is
0298  *                 appended here, otherwise it is not touched
0299  *
0300  * @return true if rename successful.
0301  */
0302 bool DirRenamer::renameDirectory(
0303   const QString& olddir, const QString& newdir,
0304   const QPersistentModelIndex& index, QString* errorMsg) const
0305 {
0306   if (QFileInfo::exists(newdir)) {
0307     if (errorMsg) {
0308       errorMsg->append(tr("File %1 already exists\n").arg(newdir));
0309     }
0310     return false;
0311   }
0312   if (!QFileInfo(olddir).isDir()) {
0313     if (errorMsg) {
0314       errorMsg->append(tr("%1 is not a folder\n").arg(olddir));
0315     }
0316     return false;
0317   }
0318   if (index.isValid()) {
0319     // The directory must be closed before renaming on Windows.
0320     TaggedFileIterator::closeFileHandles(index);
0321   }
0322   if (auto model = const_cast<TaggedFileSystemModel*>(
0323         qobject_cast<const TaggedFileSystemModel*>(index.model()))) {
0324     const QString parentDirName = model->filePath(index.parent());
0325     if (const QString relativeName = QDir(parentDirName).relativeFilePath(newdir);
0326         model->rename(index, relativeName) && QFileInfo(newdir).isDir()) {
0327       return true;
0328     }
0329   }
0330   if (Utils::safeRename(olddir, newdir) && QFileInfo(newdir).isDir()) {
0331     return true;
0332   }
0333   if (errorMsg) {
0334     errorMsg->append(tr("Rename %1 to %2 failed\n").arg(olddir, newdir));
0335   }
0336   return false;
0337 }
0338 
0339 /** Only defined for generation of translation files */
0340 #define ALREADY_EXISTS_FOR_PO QT_TRANSLATE_NOOP("@default", "%1 already exists\n")
0341 /** Only defined for generation of translation files */
0342 #define IS_NOT_FILE_FOR_PO QT_TRANSLATE_NOOP("@default", "%1 is not a file\n")
0343 
0344 /**
0345  * Rename a file.
0346  *
0347  * @param oldfn    old file name
0348  * @param newfn    new file name
0349  * @param errorMsg if not NULL and an error occurred, a message is
0350  *                 appended here, otherwise it is not touched
0351  * @param index    model index of item to rename
0352  *
0353  * @return true if rename successful or newfn already exists.
0354  */
0355 bool DirRenamer::renameFile(const QString& oldfn, const QString& newfn,
0356                 const QPersistentModelIndex& index, QString* errorMsg) const
0357 {
0358   if (QFileInfo(newfn).isFile()) {
0359     return true;
0360   }
0361   if (QFileInfo::exists(newfn)) {
0362     if (errorMsg) {
0363       errorMsg->append(tr("%1 already exists\n").arg(newfn));
0364     }
0365     return false;
0366   }
0367   if (!QFileInfo(oldfn).isFile()) {
0368     if (errorMsg) {
0369       errorMsg->append(tr("%1 is not a file\n").arg(oldfn));
0370     }
0371     return false;
0372   }
0373   if (TaggedFile* taggedFile =
0374       TaggedFileSystemModel::getTaggedFileOfIndex(index)) {
0375     // The file must be closed before renaming on Windows.
0376     taggedFile->closeFileHandle();
0377   }
0378   if (Utils::safeRename(oldfn, newfn) && QFileInfo(newfn).isFile()) {
0379     return true;
0380   }
0381   if (errorMsg) {
0382     errorMsg->append(tr("Rename %1 to %2 failed\n").arg(oldfn, newfn));
0383   }
0384   return false;
0385 }
0386 
0387 /**
0388  * Generate new directory name according to current settings.
0389  *
0390  * @param taggedFile file to get information from
0391  * @param olddir pointer to QString to place old directory name into,
0392  *               NULL if not used
0393  *
0394  * @return new directory name.
0395  */
0396 QString DirRenamer::generateNewDirname(TaggedFile* taggedFile, QString* olddir)
0397 {
0398   taggedFile->readTags(false);
0399   TrackData trackData(*taggedFile, m_tagVersion);
0400   QString newdir(taggedFile->getDirname());
0401 #ifdef Q_OS_WIN32
0402   newdir.replace(QLatin1Char('\\'), QLatin1Char('/'));
0403 #endif
0404   if (newdir.endsWith(QLatin1Char('/'))) {
0405     // remove trailing separator
0406     newdir.truncate(newdir.length() - 1);
0407   }
0408   if (olddir) {
0409     *olddir = newdir;
0410   }
0411   if (!trackData.isEmptyOrInactive()) {
0412     if (!m_actionCreate) {
0413       newdir = parentDirectory(newdir);
0414     } else if (!newdir.isEmpty()) {
0415       newdir.append(QLatin1Char('/'));
0416     }
0417     DirNameFormatReplacer fmt(*m_fmtContext, trackData, m_format);
0418     fmt.replacePercentCodes(FormatReplacer::FSF_ReplaceSeparators);
0419     QString baseName = fmt.getString();
0420     if (FormatConfig& fnCfg = FilenameFormatConfig::instance();
0421         fnCfg.useForOtherFileNames()) {
0422       bool isFilenameFormatter = fnCfg.switchFilenameFormatter(false);
0423       if (!baseName.contains(QLatin1Char('/'))) {
0424         fnCfg.formatString(baseName);
0425       } else {
0426         // If the new folder name contains multiple path components separated
0427         // by '/', make sure not to replace the '/' when applying the format.
0428         QStringList baseNameComponents = baseName.split(QLatin1Char('/'));
0429         for (auto it = baseNameComponents.begin();
0430              it != baseNameComponents.end();
0431              ++it) {
0432           fnCfg.formatString(*it);
0433         }
0434         baseName = baseNameComponents.join(QLatin1Char('/'));
0435       }
0436       fnCfg.switchFilenameFormatter(isFilenameFormatter);
0437     }
0438     m_fmtContext->putDirName(baseName);
0439     newdir.append(
0440           FilenameFormatConfig::instance().joinFileName(baseName, QString()));
0441   }
0442   return newdir;
0443 }
0444 
0445 /**
0446  * Clear the rename actions.
0447  * This method has to be called before scheduling new actions.
0448  */
0449 void DirRenamer::clearActions()
0450 {
0451   m_actions.clear();
0452 }
0453 
0454 /**
0455  * Add a rename action.
0456  *
0457  * @param type type of action
0458  * @param src  source file or directory name
0459  * @param dest destination file or directory name
0460  * @param index model index of item to rename
0461  */
0462 void DirRenamer::addAction(RenameAction::Type type, const QString& src, const QString& dest,
0463                            const QPersistentModelIndex& index)
0464 {
0465   // do not add an action if the source or destination is already in an action
0466   for (auto it = m_actions.constBegin(); it != m_actions.constEnd(); ++it) {
0467     if ((!src.isEmpty() && it->m_src == src) ||
0468         (!dest.isEmpty() && it->m_dest == dest)){
0469       return;
0470     }
0471   }
0472 
0473   RenameAction action(type, src, dest, index);
0474   m_actions.append(action);
0475   if (!m_fmtContext->hasAggregatedCodes()) {
0476     emit actionScheduled(describeAction(action));
0477   }
0478 }
0479 
0480 /**
0481  * Add a rename action.
0482  *
0483  * @param type type of action
0484  * @param dest destination file or directory name
0485  */
0486 void DirRenamer::addAction(RenameAction::Type type, const QString& dest)
0487 {
0488   addAction(type, QString(), dest);
0489 }
0490 
0491 /**
0492  * Check if there is already an action scheduled for this source.
0493  *
0494  * @return true if a rename action for the source exists.
0495  */
0496 bool DirRenamer::actionHasSource(const QString& src) const
0497 {
0498   if (src.isEmpty()) {
0499     return false;
0500   }
0501   for (auto it = m_actions.constBegin(); it != m_actions.constEnd(); ++it) {
0502     if (it->m_src == src) {
0503       return true;
0504     }
0505   }
0506   return false;
0507 }
0508 
0509 /**
0510  * Check if there is already an action scheduled for this destination.
0511  *
0512  * @return true if a rename or create action for the destination exists.
0513  */
0514 bool DirRenamer::actionHasDestination(const QString& dest) const
0515 {
0516   if (dest.isEmpty()) {
0517     return false;
0518   }
0519   for (auto it = m_actions.constBegin(); it != m_actions.constEnd(); ++it) {
0520     if (it->m_dest == dest) {
0521       return true;
0522     }
0523   }
0524   return false;
0525 }
0526 
0527 /**
0528  * Replace directory name if there is already a rename action.
0529  *
0530  * @param src directory name, will be replaced if there is a rename action
0531  */
0532 void DirRenamer::replaceIfAlreadyRenamed(QString& src) const
0533 {
0534   bool found = true;
0535   for (int i = 0; found && i <  5; ++i) {
0536     found = false;
0537     for (auto it = m_actions.constBegin(); it != m_actions.constEnd(); ++it) {
0538       if (it->m_type == RenameAction::RenameDirectory &&
0539           it->m_src == src) {
0540         src = it->m_dest;
0541         found = true;
0542         break;
0543       }
0544     }
0545   }
0546 }
0547 
0548 /**
0549  * Schedule the actions necessary to rename the directory containing a file.
0550  *
0551  * @param taggedFile file in directory
0552  */
0553 void DirRenamer::scheduleAction(TaggedFile* taggedFile)
0554 {
0555   QString currentDirname;
0556   QString newDirname(generateNewDirname(taggedFile, &currentDirname));
0557   bool again = false;
0558   for (int round = 0; round < 2; ++round) {
0559     replaceIfAlreadyRenamed(currentDirname);
0560     if (newDirname != currentDirname) {
0561       if (newDirname.startsWith(currentDirname + QLatin1Char('/'))) {
0562         // A new directory is created in the current directory.
0563         bool createDir = true;
0564         QString dirWithFiles(currentDirname);
0565         for (int i = 0;
0566              createDir && newDirname.startsWith(currentDirname) && i < 5;
0567              i++) {
0568           QString newPart(newDirname.mid(currentDirname.length()));
0569           // currentDirname does not end with a separator, so newPart
0570           // starts with a separator and the search starts with the
0571           // second character.
0572           if (int slashPos = newPart.indexOf(QLatin1Char('/'), 1);
0573               slashPos != -1 && slashPos != newPart.length() - 1) {
0574             newPart.truncate(slashPos);
0575             // the new part has multiple directories
0576             // => create one directory
0577           } else {
0578             createDir = false;
0579           }
0580           // Create a directory for each file and move it.
0581           addAction(RenameAction::CreateDirectory, QString(),
0582                     currentDirname + newPart, taggedFile->getIndex());
0583           if (!createDir) {
0584             addAction(RenameAction::RenameFile,
0585                       dirWithFiles + QLatin1Char('/') + taggedFile->getFilename(),
0586                       currentDirname + newPart + QLatin1Char('/') + taggedFile->getFilename(),
0587                       taggedFile->getIndex());
0588           }
0589           currentDirname = currentDirname + newPart;
0590         }
0591       } else {
0592         if (QString parent(parentDirectory(currentDirname));
0593             newDirname.startsWith(parent)) {
0594           QString newPart(newDirname.mid(parent.length()));
0595           if (int slashPos = newPart.indexOf(QLatin1Char('/'));
0596               slashPos != -1 && slashPos != newPart.length() - 1) {
0597             newPart.truncate(slashPos);
0598             // the new part has multiple directories
0599             // => rename current directory, then create additional
0600             // directories.
0601             again = true;
0602           }
0603           if (QString parentWithNewPart = parent + newPart;
0604               (QFileInfo(parentWithNewPart).isDir() &&
0605                !actionHasSource(parentWithNewPart)) ||
0606               actionHasDestination(parentWithNewPart)) {
0607             // directory already exists => move files
0608             addAction(RenameAction::RenameFile,
0609                       currentDirname + QLatin1Char('/') + taggedFile->getFilename(),
0610                       parentWithNewPart + QLatin1Char('/') + taggedFile->getFilename(),
0611                       taggedFile->getIndex());
0612             currentDirname = parentWithNewPart;
0613           } else {
0614             addAction(RenameAction::RenameDirectory, currentDirname, parentWithNewPart,
0615                       taggedFile->getIndex().parent());
0616             currentDirname = parentWithNewPart;
0617           }
0618         } else {
0619           // new directory name is too different
0620           addAction(RenameAction::ReportError, tr("New folder name is too different\n"));
0621         }
0622       }
0623     }
0624     if (!again) break;
0625   }
0626 }
0627 
0628 /**
0629  * Terminate scheduling of actions.
0630  */
0631 void DirRenamer::endScheduleActions()
0632 {
0633   if (m_fmtContext->hasAggregatedCodes()) {
0634     const auto replacements = m_fmtContext->takeReplacements();
0635     for (RenameAction& action : m_actions) {
0636       for (const auto& replacement : replacements) {
0637         action.m_src.replace(replacement.first, replacement.second);
0638         action.m_dest.replace(replacement.first, replacement.second);
0639       }
0640       emit actionScheduled(describeAction(action));
0641     }
0642   }
0643 }
0644 
0645 /**
0646  * Perform the scheduled rename actions.
0647  *
0648  * @param errorMsg if not 0 and an error occurred, a message is appended here,
0649  *                 otherwise it is not touched
0650  */
0651 void DirRenamer::performActions(QString* errorMsg)
0652 {
0653   for (auto it = m_actions.constBegin(); it != m_actions.constEnd(); ++it) {
0654     switch (it->m_type) {
0655       case RenameAction::CreateDirectory:
0656         createDirectory(it->m_dest, it->m_index, errorMsg);
0657         break;
0658       case RenameAction::RenameDirectory:
0659         if (renameDirectory(it->m_src, it->m_dest, it->m_index,
0660                             errorMsg)) {
0661           if (it->m_src == m_dirName) {
0662             m_dirName = it->m_dest;
0663           }
0664         }
0665         break;
0666       case RenameAction::RenameFile:
0667         renameFile(it->m_src, it->m_dest, it->m_index, errorMsg);
0668         break;
0669       case RenameAction::ReportError:
0670       default:
0671         if (errorMsg) {
0672           *errorMsg += it->m_dest;
0673         }
0674     }
0675   }
0676 }
0677 
0678 /**
0679  * Get description of an actions to be performed.
0680  * @return (action, [src,] dst) list describing the action to be
0681  * performed.
0682  */
0683 QStringList DirRenamer::describeAction(const RenameAction& action) const
0684 {
0685   static const char* const typeStr[] = {
0686     QT_TRANSLATE_NOOP("@default", "Create folder"),
0687     QT_TRANSLATE_NOOP("@default", "Rename folder"),
0688     QT_TRANSLATE_NOOP("@default", "Rename file"),
0689     QT_TRANSLATE_NOOP("@default", "Error")
0690   };
0691   static constexpr unsigned numTypeStr = std::size(typeStr);
0692 
0693   QStringList actionStrs;
0694   auto typeIdx = static_cast<unsigned>(action.m_type);
0695   if (typeIdx >= numTypeStr) {
0696     typeIdx = numTypeStr - 1;
0697   }
0698   actionStrs.append(QCoreApplication::translate("@default", typeStr[typeIdx]));
0699   if (!action.m_src.isEmpty()) {
0700     actionStrs.append(action.m_src);
0701   }
0702   actionStrs.append(action.m_dest);
0703   return actionStrs;
0704 }
0705 
0706 /**
0707  * Check if operation is aborted.
0708  *
0709  * @return true if aborted.
0710  */
0711 bool DirRenamer::isAborted() const
0712 {
0713   return m_aborted;
0714 }
0715 
0716 /**
0717  * Clear state which is reported by isAborted().
0718  */
0719 void DirRenamer::clearAborted()
0720 {
0721   m_aborted = false;
0722 }
0723 
0724 /**
0725  * Abort operation.
0726  */
0727 void DirRenamer::abort()
0728 {
0729   m_aborted = true;
0730 }