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

0001 /**
0002  * \file kid3application.cpp
0003  * Kid3 application logic, independent of GUI.
0004  *
0005  * \b Project: Kid3
0006  * \author Urs Fleisch
0007  * \date 10 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 "kid3application.h"
0028 #include <cerrno>
0029 #include <cstring>
0030 #if QT_VERSION >= 0x060000
0031 #include <QStringConverter>
0032 #else
0033 #include <QTextCodec>
0034 #endif
0035 #include <QTextStream>
0036 #include <QNetworkAccessManager>
0037 #include <QTimer>
0038 #include <QCoreApplication>
0039 #include <QPluginLoader>
0040 #include <QElapsedTimer>
0041 #include <QUrl>
0042 #ifdef Q_OS_MAC
0043 #include <CoreFoundation/CFURL.h>
0044 #endif
0045 #ifdef Q_OS_ANDROID
0046 #include <QStandardPaths>
0047 #endif
0048 #if defined Q_OS_LINUX && !defined Q_OS_ANDROID
0049 #include <malloc.h>
0050 #endif
0051 #ifdef HAVE_QTDBUS
0052 #include <QDBusConnection>
0053 #include <unistd.h>
0054 #include "scriptinterface.h"
0055 #endif
0056 #include "icoreplatformtools.h"
0057 #include "fileproxymodeliterator.h"
0058 #include "filefilter.h"
0059 #include "modeliterator.h"
0060 #include "trackdatamodel.h"
0061 #include "timeeventmodel.h"
0062 #include "frameobjectmodel.h"
0063 #include "playlistmodel.h"
0064 #include "imagedataprovider.h"
0065 #include "pictureframe.h"
0066 #include "textimporter.h"
0067 #include "importparser.h"
0068 #include "textexporter.h"
0069 #include "serverimporter.h"
0070 #include "saferename.h"
0071 #include "configstore.h"
0072 #include "formatconfig.h"
0073 #include "tagconfig.h"
0074 #include "fileconfig.h"
0075 #include "importconfig.h"
0076 #include "guiconfig.h"
0077 #include "playlistconfig.h"
0078 #include "isettings.h"
0079 #include "playlistcreator.h"
0080 #include "iframeeditor.h"
0081 #include "batchimportprofile.h"
0082 #include "batchimportconfig.h"
0083 #include "iserverimporterfactory.h"
0084 #include "iservertrackimporterfactory.h"
0085 #include "itaggedfilefactory.h"
0086 #include "iusercommandprocessor.h"
0087 #ifdef Q_OS_ANDROID
0088 #include "androidutils.h"
0089 #endif
0090 #include "importplugins.h"
0091 
0092 namespace {
0093 
0094 /**
0095  * Get the file name of the plugin from the plugin name.
0096  * @param pluginName name of the plugin
0097  * @return file name.
0098  */
0099 QString pluginFileName(const QString& pluginName)
0100 {
0101   QString fileName = pluginName.toLower();
0102 #ifdef Q_OS_WIN32
0103 #ifdef Q_CC_MSVC
0104   fileName += QLatin1String(".dll");
0105 #else
0106   fileName = QLatin1String("lib") + fileName + QLatin1String(".dll");
0107 #endif
0108 #elif defined Q_OS_MAC
0109   fileName = QLatin1String("lib") + fileName + QLatin1String(".dylib");
0110 #else
0111   fileName = QLatin1String("lib") + fileName + QLatin1String(".so");
0112 #endif
0113   return fileName;
0114 }
0115 
0116 /**
0117  * Get text encoding from tag config as frame text encoding.
0118  * @return frame text encoding.
0119  */
0120 Frame::TextEncoding frameTextEncodingFromConfig()
0121 {
0122   Frame::TextEncoding encoding;
0123   switch (TagConfig::instance().textEncoding()) {
0124   case TagConfig::TE_UTF16:
0125     encoding = Frame::TE_UTF16;
0126     break;
0127   case TagConfig::TE_UTF8:
0128     encoding = Frame::TE_UTF8;
0129     break;
0130   case TagConfig::TE_ISO8859_1:
0131   default:
0132     encoding = Frame::TE_ISO8859_1;
0133   }
0134   return encoding;
0135 }
0136 
0137 /**
0138  * Extract file path, field name and index from frame name.
0139  *
0140  * @param frameName frame name with additional information for file, field and
0141  * index
0142  * @param dataFileName the path to a data file is returned here if available,
0143  * else null
0144  * @param fieldName the field name is returned here if available, else null
0145  * @param index the index is returned here if available, else 0
0146  */
0147 void extractFileFieldIndex(
0148     QString& frameName, QString& dataFileName, QString& fieldName, int& index)
0149 {
0150   dataFileName.clear();
0151   fieldName.clear();
0152   index = 0;
0153   if (int colonIndex = frameName.indexOf(QLatin1Char(':')); colonIndex != -1) {
0154     dataFileName = frameName.mid(colonIndex + 1);
0155     frameName.truncate(colonIndex);
0156   }
0157   if (int dotIndex = frameName.indexOf(QLatin1Char('.')); dotIndex != -1) {
0158     fieldName = frameName.mid(dotIndex + 1);
0159     frameName.truncate(dotIndex);
0160   }
0161   if (int bracketIndex = frameName.indexOf(QLatin1Char('[')); bracketIndex != -1) {
0162     if (const int closingBracketIndex =
0163           frameName.indexOf(QLatin1Char(']'), bracketIndex + 1);
0164         closingBracketIndex > bracketIndex) {
0165       bool ok;
0166 #if QT_VERSION >= 0x060000
0167       index = frameName.mid(
0168           bracketIndex + 1, closingBracketIndex - bracketIndex - 1).toInt(&ok);
0169 #else
0170       index = frameName.midRef(
0171           bracketIndex + 1, closingBracketIndex - bracketIndex - 1).toInt(&ok);
0172 #endif
0173       if (ok) {
0174         frameName.remove(bracketIndex, closingBracketIndex - bracketIndex + 1);
0175       }
0176     }
0177   }
0178 }
0179 
0180 /**
0181  * Get the internal rating frame name with optional field.
0182  * @param frame frame containing rating
0183  * @param taggedFile optional taggedFile to be used if @a frame does not have
0184  *        a useful internal name
0185  * @param tagNr used together with @a taggedFile to guess rating name
0186  * @return internal name, "POPM.Email-Value" if POPM with Email value.
0187  */
0188 QString ratingTypeName(const Frame& frame,
0189                        const TaggedFile* taggedFile = nullptr,
0190                        Frame::TagNumber tagNr = Frame::Tag_2)
0191 {
0192   QString name = frame.getInternalName();
0193   if (name.startsWith(QLatin1String("POPM"))) {
0194     name.truncate(4);
0195     QVariant emailVar = frame.getFieldValue(Frame::ID_Email);
0196     if (QString emailValue;
0197         emailVar.isValid() &&
0198         !(emailValue = emailVar.toString()).isEmpty()) {
0199       name += QLatin1Char('.');
0200       name += emailValue;
0201     }
0202   } else if (taggedFile &&
0203              name != QLatin1String("RATING") &&
0204              name != QLatin1String("rate") &&
0205              name != QLatin1String("IRTD") &&
0206              name != QLatin1String("WM/SharedUserRating")) {
0207     QString tagFormat = taggedFile->getTagFormat(tagNr);
0208     if (tagFormat.isEmpty()) {
0209       if (QString ext = taggedFile->getFileExtension().toLower();
0210           ext == QLatin1String(".mp3") || ext == QLatin1String(".mp2") ||
0211           ext == QLatin1String(".aac") || ext == QLatin1String(".tta") ||
0212           ext == QLatin1String(".dsf") || ext == QLatin1String(".dff")) {
0213         tagFormat = QLatin1String("ID3v2.3.0");
0214       } else if (ext == QLatin1String(".ogg") ||
0215                  ext == QLatin1String(".flac") ||
0216                  ext == QLatin1String(".opus")) {
0217         tagFormat = QLatin1String("Vorbis");
0218       } else if (ext == QLatin1String(".m4a")) {
0219         tagFormat = QLatin1String("MP4");
0220       } else if (ext == QLatin1String(".wav") ||
0221                  ext == QLatin1String(".aiff")) {
0222         tagFormat = tagNr == Frame::Tag_3 ? QLatin1String("RIFF INFO")
0223                                           : QLatin1String("ID3v2.3.0");
0224       } else if (ext == QLatin1String(".wma")) {
0225         tagFormat = QLatin1String("ASF");
0226       }
0227     }
0228     if (tagFormat.startsWith(QLatin1String("ID3v2"))) {
0229       name = QLatin1String("POPM");
0230     } else if (tagFormat == QLatin1String("Vorbis")) {
0231       name = QLatin1String("RATING");
0232     } else if (tagFormat == QLatin1String("MP4")) {
0233       name = QLatin1String("rate");
0234     } else if (tagFormat == QLatin1String("RIFF INFO")) {
0235       name = QLatin1String("IRTD");
0236     } else if (tagFormat == QLatin1String("ASF")) {
0237       name = QLatin1String("WM/SharedUserRating");
0238     }
0239   }
0240   return name;
0241 }
0242 
0243 }
0244 
0245 /** Fallback for path to search for plugins */
0246 QString Kid3Application::s_pluginsPathFallback;
0247 
0248 /**
0249  * Constructor.
0250  * @param platformTools platform tools
0251  * @param parent parent object
0252  */
0253 Kid3Application::Kid3Application(ICorePlatformTools* platformTools,
0254                                  QObject* parent) : QObject(parent),
0255   m_platformTools(platformTools),
0256   m_configStore(new ConfigStore(m_platformTools->applicationSettings())),
0257   m_fileSystemModel(new TaggedFileSystemModel(m_platformTools->iconProvider(), this)),
0258   m_fileProxyModel(new FileProxyModel(this)),
0259   m_fileProxyModelIterator(new FileProxyModelIterator(m_fileProxyModel)),
0260   m_dirProxyModel(new DirProxyModel(this)),
0261   m_fileSelectionModel(new QItemSelectionModel(m_fileProxyModel, this)),
0262   m_dirSelectionModel(new QItemSelectionModel(m_dirProxyModel, this)),
0263   m_trackDataModel(new TrackDataModel(m_platformTools->iconProvider(), this)),
0264   m_netMgr(new QNetworkAccessManager(this)),
0265   m_downloadClient(new DownloadClient(m_netMgr)),
0266   m_textExporter(new TextExporter(this)),
0267   m_tagSearcher(new TagSearcher(this)),
0268   m_dirRenamer(new DirRenamer(this)),
0269   m_batchImporter(new BatchImporter(m_netMgr)),
0270   m_player(nullptr),
0271   m_expressionFileFilter(nullptr),
0272   m_downloadImageDest(ImageForSelectedFiles),
0273   m_fileFilter(nullptr), m_filterPassed(0), m_filterTotal(0),
0274   m_batchImportProfile(nullptr), m_batchImportTagVersion(Frame::TagNone),
0275   m_editFrameTaggedFile(nullptr), m_addFrameTaggedFile(nullptr),
0276   m_frameEditor(nullptr), m_storedFrameEditor(nullptr),
0277   m_imageProvider(nullptr),
0278 #ifdef Q_OS_ANDROID
0279   m_pendingIntentsChecked(false),
0280 #endif
0281 #ifdef HAVE_QTDBUS
0282   m_dbusEnabled(false),
0283 #endif
0284   m_filtered(false), m_selectionOperationRunning(false)
0285 {
0286   const TagConfig& tagCfg = TagConfig::instance();
0287   FOR_ALL_TAGS(tagNr) {
0288     bool id3v1 = tagNr == Frame::Tag_Id3v1;
0289     m_genreModel[tagNr] = new GenreModel(id3v1, this);
0290     m_framesModel[tagNr] = new FrameTableModel(
0291           id3v1, platformTools->iconProvider(), this);
0292     if (!id3v1) {
0293       m_framesModel[tagNr]->setFrameOrder(tagCfg.quickAccessFrameOrder());
0294       connect(&tagCfg, &TagConfig::quickAccessFrameOrderChanged,
0295               m_framesModel[tagNr], &FrameTableModel::setFrameOrder);
0296     }
0297     m_framesSelectionModel[tagNr] = new QItemSelectionModel(m_framesModel[tagNr], this);
0298     m_framelist[tagNr] = new FrameList(tagNr, m_framesModel[tagNr],
0299         m_framesSelectionModel[tagNr]);
0300     connect(m_framelist[tagNr], &FrameList::frameEdited,
0301             this, &Kid3Application::onFrameEdited);
0302     connect(m_framelist[tagNr], &FrameList::frameAdded,
0303             this, &Kid3Application::onTag2FrameAdded);
0304     m_tagContext[tagNr] = new Kid3ApplicationTagContext(this, tagNr);
0305   }
0306   m_selection = new TaggedFileSelection(m_framesModel, this);
0307   setObjectName(QLatin1String("Kid3Application"));
0308   m_fileSystemModel->setReadOnly(false);
0309   const FileConfig& fileCfg = FileConfig::instance();
0310   m_fileSystemModel->setSortIgnoringPunctuation(fileCfg.sortIgnoringPunctuation());
0311   m_fileProxyModel->setSourceModel(m_fileSystemModel);
0312   m_dirProxyModel->setSourceModel(m_fileSystemModel);
0313   connect(m_fileSelectionModel,
0314           &QItemSelectionModel::selectionChanged,
0315           this, &Kid3Application::fileSelected);
0316   connect(m_fileSelectionModel,
0317           &QItemSelectionModel::selectionChanged,
0318           this, &Kid3Application::fileSelectionChanged);
0319   connect(m_fileProxyModel, &FileProxyModel::modifiedChanged,
0320           this, &Kid3Application::modifiedChanged);
0321 
0322   connect(m_selection, &TaggedFileSelection::singleFileChanged,
0323           this, &Kid3Application::updateCoverArtImageId);
0324   connect(m_selection, &TaggedFileSelection::fileNameModified,
0325           this, &Kid3Application::selectedFilesUpdated);
0326 
0327   initPlugins();
0328   m_batchImporter->setImporters(m_importers, m_trackDataModel);
0329 
0330 #ifdef Q_OS_ANDROID
0331   new AndroidUtils(this);
0332   // Make sure that configuration changes are saved for the Android app.
0333   // Old style connect syntax is used to avoid a dependency to QGuiApplication.
0334   connect(qApp, SIGNAL(applicationStateChanged(Qt::ApplicationState)),
0335           this, SLOT(onApplicationStateChanged(Qt::ApplicationState)));
0336   QObject::connect(AndroidUtils::instance(), &AndroidUtils::filePathReceived,
0337                    this, [this](const QString& path) {
0338     dropLocalFiles({path}, false);
0339   });
0340 #endif
0341 }
0342 
0343 /**
0344  * Destructor.
0345  */
0346 Kid3Application::~Kid3Application()
0347 {
0348 #ifdef Q_OS_MAC
0349   // If a song is played, then stopped and Kid3 is terminated, it will crash in
0350   // the QMediaPlayer destructor (Dispatch queue: com.apple.main-thread,
0351   // objc_msgSend() selector name: setRate). Avoid calling the destructor by
0352   // setting the QMediaPlayer's parent to null. See also:
0353   // https://qt-project.org/forums/viewthread/29651
0354   if (m_player) {
0355     m_player->setParent(0);
0356   }
0357 #endif
0358 }
0359 
0360 /**
0361  * Save config when suspended, check intents when activated.
0362  * @param state application state
0363  */
0364 void Kid3Application::onApplicationStateChanged(Qt::ApplicationState state)
0365 {
0366 #ifdef Q_OS_ANDROID
0367   if (state == Qt::ApplicationSuspended) {
0368     saveConfig();
0369   } else if (state == Qt::ApplicationActive) {
0370     // When the app becomes active for the first time,
0371     // check if it was launched with an intent.
0372     if (!m_pendingIntentsChecked) {
0373       m_pendingIntentsChecked = true;
0374       AndroidUtils::instance()->checkPendingIntents();
0375     }
0376   }
0377 #else
0378   Q_UNUSED(state)
0379 #endif
0380 }
0381 
0382 #ifdef HAVE_QTDBUS
0383 /**
0384  * Activate the D-Bus interface.
0385  * This method shall be called only once at initialization.
0386  */
0387 void Kid3Application::activateDbusInterface()
0388 {
0389   if (QDBusConnection::sessionBus().isConnected()) {
0390     QString serviceName(QLatin1String("org.kde.kid3"));
0391     QDBusConnection::sessionBus().registerService(serviceName);
0392     // For the case of multiple Kid3 instances running, register also a service
0393     // with the PID appended. On KDE such a service is already registered but
0394     // the call to registerService() seems to succeed nevertheless.
0395     serviceName += QLatin1Char('-');
0396     serviceName += QString::number(::getpid());
0397     QDBusConnection::sessionBus().registerService(serviceName);
0398     new ScriptInterface(this);
0399     if (QDBusConnection::sessionBus().registerObject(QLatin1String("/Kid3"), this)) {
0400       m_dbusEnabled = true;
0401     } else {
0402       qWarning("Registering D-Bus object failed");
0403     }
0404   } else {
0405     qWarning("Cannot connect to the D-BUS session bus.");
0406   }
0407 }
0408 #endif
0409 
0410 /**
0411  * Load and initialize plugins depending on configuration.
0412  */
0413 void Kid3Application::initPlugins()
0414 {
0415   // Load plugins, set information about plugins in configuration.
0416   ImportConfig& importCfg = ImportConfig::instance();
0417   TagConfig& tagCfg = TagConfig::instance();
0418   importCfg.clearAvailablePlugins();
0419   tagCfg.clearAvailablePlugins();
0420   const auto plugins = loadPlugins();
0421   for (QObject* plugin : plugins) {
0422     checkPlugin(plugin);
0423   }
0424   // Order the meta data plugins as configured.
0425   if (QStringList pluginOrder = tagCfg.pluginOrder(); !pluginOrder.isEmpty()) {
0426     QList<ITaggedFileFactory*> orderedFactories;
0427     for (int i = 0; i < pluginOrder.size(); ++i) {
0428       orderedFactories.append(nullptr); // clazy:exclude=reserve-candidates
0429     }
0430     const auto factories = FileProxyModel::taggedFileFactories();
0431     for (ITaggedFileFactory* factory : factories) {
0432       if (int idx = pluginOrder.indexOf(factory->name()); idx >= 0) {
0433         orderedFactories[idx] = factory;
0434       } else {
0435         orderedFactories.append(factory); // clazy:exclude=reserve-candidates
0436       }
0437     }
0438     orderedFactories.removeAll(nullptr);
0439     FileProxyModel::taggedFileFactories().swap(orderedFactories);
0440   }
0441 }
0442 
0443 /**
0444  * Find directory containing plugins.
0445  * @param pluginsDir the plugin directory is returned here
0446  * @return true if found.
0447  */
0448 bool Kid3Application::findPluginsDirectory(QDir& pluginsDir) {
0449   // First check if we are running from the build directory to load the
0450   // plugins from there.
0451   pluginsDir.setPath(QCoreApplication::applicationDirPath());
0452   QString dirName = pluginsDir.dirName();
0453 #ifdef Q_OS_WIN
0454   QString buildType;
0455   if (dirName.compare(QLatin1String("debug"), Qt::CaseInsensitive) == 0 ||
0456       dirName.compare(QLatin1String("release"), Qt::CaseInsensitive) == 0) {
0457     buildType = dirName;
0458     pluginsDir.cdUp();
0459     dirName = pluginsDir.dirName();
0460   }
0461 #endif
0462   bool pluginsDirFound = pluginsDir.cd(QLatin1String(
0463       dirName == QLatin1String("qt") || dirName == QLatin1String("kde") ||
0464       dirName == QLatin1String("cli") || dirName == QLatin1String("qml")
0465       ? "../../plugins"
0466       : dirName == QLatin1String("test")
0467         ? "../plugins"
0468         : CFG_PLUGINSDIR));
0469 #ifdef Q_OS_MAC
0470   if (!pluginsDirFound) {
0471     pluginsDirFound = pluginsDir.cd(QLatin1String("../../../../../plugins"));
0472   }
0473 #endif
0474 #ifdef Q_OS_WIN
0475   if (pluginsDirFound && !buildType.isEmpty()) {
0476     pluginsDir.cd(buildType);
0477   }
0478 #endif
0479   return pluginsDirFound;
0480 }
0481 
0482 /**
0483  * Set fallback path for directory containing plugins.
0484  * @param path path to be searched for plugins if they are not found at the
0485  * standard location relative to the application directory
0486  */
0487 void Kid3Application::setPluginsPathFallback(const QString& path)
0488 {
0489   s_pluginsPathFallback = path;
0490 }
0491 
0492 /**
0493  * Load plugins.
0494  * @return list of plugin instances.
0495  */
0496 QObjectList Kid3Application::loadPlugins()
0497 {
0498   QObjectList plugins = QPluginLoader::staticInstances();
0499 
0500   QDir pluginsDir;
0501   bool pluginsDirFound = findPluginsDirectory(pluginsDir);
0502   if (!pluginsDirFound && !s_pluginsPathFallback.isEmpty()) {
0503     pluginsDir.setPath(s_pluginsPathFallback);
0504     pluginsDirFound = true;
0505   }
0506   if (pluginsDirFound) {
0507     ImportConfig& importCfg = ImportConfig::instance();
0508     TagConfig& tagCfg = TagConfig::instance();
0509 
0510     // Construct a set of disabled plugin file names
0511     QMap<QString, QString> disabledImportPluginFileNames;
0512     const QStringList disabledPlugins = importCfg.disabledPlugins();
0513     for (const QString& pluginName : disabledPlugins) {
0514       disabledImportPluginFileNames.insert(pluginFileName(pluginName),
0515                                            pluginName);
0516     }
0517     QMap<QString, QString> disabledTagPluginFileNames;
0518     const QStringList disabledTagPlugins = tagCfg.disabledPlugins();
0519     for (const QString& pluginName : disabledTagPlugins) {
0520       disabledTagPluginFileNames.insert(pluginFileName(pluginName),
0521                                         pluginName);
0522     }
0523 
0524     QStringList availablePlugins = importCfg.availablePlugins();
0525     QStringList availableTagPlugins = tagCfg.availablePlugins();
0526     const auto fileNames = pluginsDir.entryList(QDir::Files);
0527     for (const QString& fileName : fileNames) {
0528       if (disabledImportPluginFileNames.contains(fileName)) {
0529         availablePlugins.append(
0530               disabledImportPluginFileNames.value(fileName));
0531         continue;
0532       }
0533       if (disabledTagPluginFileNames.contains(fileName)) {
0534         availableTagPlugins.append(
0535               disabledTagPluginFileNames.value(fileName));
0536         continue;
0537       }
0538       QPluginLoader loader(pluginsDir.absoluteFilePath(fileName));
0539       if (QObject* plugin = loader.instance()) {
0540         if (QString name(plugin->objectName()); disabledPlugins.contains(name)) {
0541           availablePlugins.append(name);
0542           loader.unload();
0543         } else if (disabledTagPlugins.contains(name)) {
0544           availableTagPlugins.append(name);
0545           loader.unload();
0546         } else {
0547           plugins.append(plugin);
0548         }
0549       }
0550     }
0551     importCfg.setAvailablePlugins(availablePlugins);
0552     tagCfg.setAvailablePlugins(availableTagPlugins);
0553   }
0554   return plugins;
0555 }
0556 
0557 /**
0558  * Check type of a loaded plugin and register it.
0559  * @param plugin instance returned by plugin loader
0560  */
0561 void Kid3Application::checkPlugin(QObject* plugin)
0562 {
0563   if (auto importerFactory =
0564       qobject_cast<IServerImporterFactory*>(plugin)) {
0565     ImportConfig& importCfg = ImportConfig::instance();
0566     QStringList availablePlugins = importCfg.availablePlugins();
0567     availablePlugins.append(plugin->objectName());
0568     importCfg.setAvailablePlugins(availablePlugins);
0569     if (!importCfg.disabledPlugins().contains(plugin->objectName())) {
0570       const auto keys = importerFactory->serverImporterKeys();
0571       for (const QString& key : keys) {
0572         m_importers.append(importerFactory->createServerImporter(
0573                              key, m_netMgr, m_trackDataModel));
0574       }
0575     }
0576   }
0577   if (auto importerFactory =
0578       qobject_cast<IServerTrackImporterFactory*>(plugin)) {
0579     ImportConfig& importCfg = ImportConfig::instance();
0580     QStringList availablePlugins = importCfg.availablePlugins();
0581     availablePlugins.append(plugin->objectName());
0582     importCfg.setAvailablePlugins(availablePlugins);
0583     if (!importCfg.disabledPlugins().contains(plugin->objectName())) {
0584       const auto keys = importerFactory->serverTrackImporterKeys();
0585       for (const QString& key : keys) {
0586         m_trackImporters.append(importerFactory->createServerTrackImporter(
0587                              key, m_netMgr, m_trackDataModel));
0588       }
0589     }
0590   }
0591   if (auto taggedFileFactory =
0592       qobject_cast<ITaggedFileFactory*>(plugin)) {
0593     TagConfig& tagCfg = TagConfig::instance();
0594     QStringList availablePlugins = tagCfg.availablePlugins();
0595     availablePlugins.append(plugin->objectName());
0596     tagCfg.setAvailablePlugins(availablePlugins);
0597     if (!tagCfg.disabledPlugins().contains(plugin->objectName())) {
0598       int features = tagCfg.taggedFileFeatures();
0599       const auto keys = taggedFileFactory->taggedFileKeys();
0600       for (const QString& key : keys) {
0601         taggedFileFactory->initialize(key);
0602         features |= taggedFileFactory->taggedFileFeatures(key);
0603       }
0604       tagCfg.setTaggedFileFeatures(features);
0605       FileProxyModel::taggedFileFactories().append(taggedFileFactory);
0606     }
0607   }
0608   if (auto userCommandProcessor =
0609       qobject_cast<IUserCommandProcessor*>(plugin)) {
0610     ImportConfig& importCfg = ImportConfig::instance();
0611     QStringList availablePlugins = importCfg.availablePlugins();
0612     availablePlugins.append(plugin->objectName());
0613     importCfg.setAvailablePlugins(availablePlugins);
0614     if (!importCfg.disabledPlugins().contains(plugin->objectName())) {
0615       m_userCommandProcessors.append(userCommandProcessor);
0616     }
0617   }
0618 }
0619 
0620 /**
0621  * Get names of available server track importers.
0622  * @return list of server track importer names.
0623  */
0624 QStringList Kid3Application::getServerImporterNames() const
0625 {
0626   QStringList names;
0627   const auto importers = m_importers;
0628   for (const ServerImporter* importer : importers) {
0629     names.append(QString::fromLatin1(importer->name()));
0630   }
0631   return names;
0632 }
0633 
0634 /**
0635  * Get audio player.
0636  * @return audio player.
0637  */
0638 QObject* Kid3Application::getAudioPlayer()
0639 {
0640   if (!m_player) {
0641 #ifdef HAVE_QTDBUS
0642     m_player = m_platformTools->createAudioPlayer(this, m_dbusEnabled);
0643 #else
0644     m_player = m_platformTools->createAudioPlayer(this, false);
0645 #endif
0646   }
0647 #ifdef HAVE_QTDBUS
0648   if (m_dbusEnabled) {
0649     activateMprisInterface();
0650   }
0651 #endif
0652   return m_player;
0653 }
0654 
0655 #ifdef HAVE_QTDBUS
0656 /**
0657  * Activate the MPRIS D-Bus Interface if not already active.
0658  */
0659 void Kid3Application::activateMprisInterface()
0660 {
0661   if (!m_mprisServiceName.isEmpty() || !m_player)
0662     return;
0663 
0664   if (QDBusConnection::sessionBus().isConnected()) {
0665     m_mprisServiceName = QLatin1String("org.mpris.MediaPlayer2.kid3");
0666     bool ok = QDBusConnection::sessionBus().registerService(m_mprisServiceName);
0667     if (!ok) {
0668       // If another instance of Kid3 is already running register a service
0669       // with ".instancePID" appended, see
0670       // https://specifications.freedesktop.org/mpris-spec/latest/
0671       m_mprisServiceName += QLatin1String(".instance");
0672       m_mprisServiceName += QString::number(::getpid());
0673       ok = QDBusConnection::sessionBus().registerService(m_mprisServiceName);
0674     }
0675     if (ok) {
0676       if (!QDBusConnection::sessionBus().registerObject(
0677             QLatin1String("/org/mpris/MediaPlayer2"), m_player)) {
0678         qWarning("Registering D-Bus MPRIS object failed");
0679       }
0680     } else {
0681       m_mprisServiceName.clear();
0682       qWarning("Registering D-Bus MPRIS service failed");
0683     }
0684   } else {
0685     qWarning("Cannot connect to the D-BUS session bus.");
0686   }
0687 }
0688 
0689 /**
0690  * Deactivate the MPRIS D-Bus Interface if it is active.
0691  */
0692 void Kid3Application::deactivateMprisInterface()
0693 {
0694   if (m_mprisServiceName.isEmpty())
0695     return;
0696 
0697   if (QDBusConnection::sessionBus().isConnected()) {
0698     QDBusConnection::sessionBus().unregisterObject(
0699           QLatin1String("/org/mpris/MediaPlayer2"));
0700     if (QDBusConnection::sessionBus().unregisterService(m_mprisServiceName)) {
0701       m_mprisServiceName.clear();
0702     } else {
0703       qWarning("Unregistering D-Bus MPRIS service failed");
0704     }
0705   } else {
0706     qWarning("Cannot connect to the D-BUS session bus.");
0707   }
0708 }
0709 #endif
0710 
0711 /**
0712  * Get settings.
0713  * @return settings.
0714  */
0715 ISettings* Kid3Application::getSettings() const
0716 {
0717   return m_platformTools->applicationSettings();
0718 }
0719 
0720 /**
0721  * Apply configuration changes.
0722  */
0723 void Kid3Application::applyChangedConfiguration()
0724 {
0725   saveConfig();
0726   const FileConfig& fileCfg = FileConfig::instance();
0727   FOR_ALL_TAGS(tagNr) {
0728     if (!TagConfig::instance().markTruncations()) {
0729       m_framesModel[tagNr]->markRows(0);
0730     }
0731     if (!fileCfg.markChanges()) {
0732       m_framesModel[tagNr]->markChangedFrames(QList<Frame::ExtendedType>());
0733     }
0734     m_genreModel[tagNr]->init();
0735   }
0736   notifyConfigurationChange();
0737 
0738   const TagConfig& tagCfg = TagConfig::instance();
0739   if (quint64 oldQuickAccessFrames = FrameCollection::getQuickAccessFrames();
0740       tagCfg.quickAccessFrames() != oldQuickAccessFrames) {
0741     FrameCollection::setQuickAccessFrames(tagCfg.quickAccessFrames());
0742     emit selectedFilesUpdated();
0743   }
0744   if (Frame::setNamesForCustomFrames(tagCfg.customFrames())) {
0745     emit selectedFilesUpdated();
0746   }
0747 
0748   QStringList nameFilters(m_platformTools->getNameFilterPatterns(
0749                             fileCfg.nameFilter()).split(QLatin1Char(' ')));
0750   m_fileProxyModel->setNameFilters(nameFilters);
0751   m_fileProxyModel->setFolderFilters(fileCfg.includeFolders(),
0752                                      fileCfg.excludeFolders());
0753 
0754   QDir::Filters oldFilter = m_fileSystemModel->filter();
0755   QDir::Filters filter = oldFilter;
0756   if (fileCfg.showHiddenFiles()) {
0757     filter |= QDir::Hidden;
0758   } else {
0759     filter &= ~QDir::Hidden;
0760   }
0761   if (filter != oldFilter) {
0762     m_fileSystemModel->setFilter(filter);
0763   }
0764 }
0765 
0766 /**
0767  * Save settings to the configuration.
0768  */
0769 void Kid3Application::saveConfig()
0770 {
0771   if (FileConfig::instance().loadLastOpenedFile()) {
0772     FileConfig::instance().setLastOpenedFile(
0773         m_fileProxyModel->filePath(currentOrRootIndex()));
0774   }
0775   m_configStore->writeToConfig();
0776   getSettings()->sync();
0777 }
0778 
0779 /**
0780  * Read settings from the configuration.
0781  */
0782 void Kid3Application::readConfig()
0783 {
0784   if (FileConfig::instance().nameFilter().isEmpty()) {
0785     setAllFilesFileFilter();
0786   }
0787   notifyConfigurationChange();
0788 
0789   const TagConfig& tagCfg = TagConfig::instance();
0790   FrameCollection::setQuickAccessFrames(tagCfg.quickAccessFrames());
0791   Frame::setNamesForCustomFrames(tagCfg.customFrames());
0792 }
0793 
0794 /**
0795  * Open directory.
0796  * When finished directoryOpened() is emitted, also if false is returned.
0797  *
0798  * @param paths file or directory paths, if multiple paths are given, the
0799  * common directory is opened and the files are selected
0800  * @param fileCheck if true, only open directory if paths exist
0801  *
0802  * @return true if ok.
0803  */
0804 bool Kid3Application::openDirectory(const QStringList& paths, bool fileCheck)
0805 {
0806 #ifdef Q_OS_ANDROID
0807   const QStringList musicLocations =
0808       QStandardPaths::standardLocations(QStandardPaths::MusicLocation).mid(0, 1);
0809   const QStringList pathList = paths.isEmpty() ? musicLocations : paths;
0810 #else
0811   const QStringList pathList(paths);
0812 #endif
0813   bool ok = true;
0814   QStringList filePaths;
0815   QStringList dirComponents;
0816   for (QString path : pathList) {
0817     if (!path.isEmpty()) {
0818       QFileInfo fileInfo(path);
0819       if (path.startsWith(QLatin1Char(':')) && !fileInfo.exists()) {
0820         // QFileInfo assumes that paths starting with a colon are absolute
0821         // and denote a QResource. Since no such file was found, try again
0822         // with a relative path to a file starting with a colon.
0823         path = QLatin1String("./") + path;
0824         fileInfo.setFile(path);
0825       }
0826       if (fileCheck && !fileInfo.exists()) {
0827         ok = false;
0828         break;
0829       }
0830       QString dirPath;
0831       if (!fileInfo.isDir()) {
0832         dirPath = fileInfo.absolutePath();
0833         if (fileInfo.isFile()) {
0834           filePaths.append(fileInfo.absoluteFilePath());
0835         }
0836       } else {
0837         dirPath = QDir(path).absolutePath();
0838       }
0839       // absolutePath() returns paths with '/' separators also on Windows
0840       // where QDir::separator() is '\'.
0841       QStringList dirPathComponents = dirPath.split(QLatin1Char('/'));
0842       if (dirComponents.isEmpty()) {
0843         dirComponents = dirPathComponents;
0844       } else {
0845         // Reduce dirPath to common prefix.
0846         auto dirIt = dirComponents.begin();
0847         auto dirPathIt = dirPathComponents.constBegin();
0848         while (dirIt != dirComponents.end() &&
0849                dirPathIt != dirPathComponents.constEnd() &&
0850                *dirIt == *dirPathIt) {
0851           ++dirIt;
0852           ++dirPathIt;
0853         }
0854         dirComponents.erase(dirIt, dirComponents.end());
0855       }
0856     }
0857   }
0858   QString dir;
0859   if (ok) {
0860     dir = dirComponents.join(QDir::separator());
0861     if (dir.isEmpty() && !filePaths.isEmpty()) {
0862       dir = QDir::rootPath();
0863     }
0864     ok = !dir.isEmpty();
0865   }
0866   QModelIndex rootIndex;
0867   QModelIndexList fileIndexes;
0868   if (ok) {
0869     const FileConfig& fileCfg = FileConfig::instance();
0870     QStringList nameFilters(m_platformTools->getNameFilterPatterns(
0871                               fileCfg.nameFilter()).split(QLatin1Char(' ')));
0872     m_fileProxyModel->setNameFilters(nameFilters);
0873     m_fileProxyModel->setFolderFilters(fileCfg.includeFolders(),
0874                                        fileCfg.excludeFolders());
0875     QDir::Filters filter = QDir::AllEntries | QDir::AllDirs;
0876     if (fileCfg.showHiddenFiles()) {
0877       filter |= QDir::Hidden;
0878     }
0879     m_fileSystemModel->setFilter(filter);
0880     rootIndex = m_fileSystemModel->setRootPath(dir);
0881     fileIndexes.reserve(filePaths.size());
0882     const auto constFilePaths = filePaths;
0883     for (const QString& filePath : constFilePaths) {
0884       fileIndexes.append(m_fileSystemModel->index(filePath));
0885     }
0886     ok = rootIndex.isValid();
0887   }
0888   if (ok) {
0889     setFiltered(false);
0890     m_dirName = dir;
0891     emit dirNameChanged(m_dirName);
0892     QModelIndex oldRootIndex = m_fileProxyModelRootIndex;
0893     m_fileProxyModelRootIndex = m_fileProxyModel->mapFromSource(rootIndex);
0894     m_fileProxyModelFileIndexes.clear();
0895     const auto constFileIndexes = fileIndexes;
0896     for (const QModelIndex& fileIndex : constFileIndexes) {
0897       m_fileProxyModelFileIndexes.append(
0898             m_fileProxyModel->mapFromSource(fileIndex));
0899     }
0900     if (m_fileProxyModelRootIndex != oldRootIndex) {
0901       connect(m_fileProxyModel, &FileProxyModel::sortingFinished,
0902               this, &Kid3Application::onDirectoryLoaded);
0903     } else {
0904       QTimer::singleShot(0, this, &Kid3Application::onDirectoryOpened);
0905     }
0906   }
0907   if (!ok) {
0908     QTimer::singleShot(0, this, &Kid3Application::onDirectoryOpened);
0909   }
0910   return ok;
0911 }
0912 
0913 /**
0914  * Update selection and emit signals when directory is opened.
0915  */
0916 void Kid3Application::onDirectoryOpened()
0917 {
0918   QModelIndex fsRoot = m_fileProxyModel->mapToSource(m_fileProxyModelRootIndex);
0919   m_dirProxyModelRootIndex = m_dirProxyModel->mapFromSource(fsRoot);
0920 
0921   emit fileRootIndexChanged(m_fileProxyModelRootIndex);
0922   emit dirRootIndexChanged(m_dirProxyModelRootIndex);
0923 
0924   if (m_fileProxyModelRootIndex.isValid()) {
0925     m_fileSelectionModel->clearSelection();
0926     if (!m_fileProxyModelFileIndexes.isEmpty()) {
0927       const auto fileIndexes = m_fileProxyModelFileIndexes;
0928       for (const QPersistentModelIndex& fileIndex : fileIndexes) {
0929         m_fileSelectionModel->select(fileIndex,
0930             QItemSelectionModel::Select | QItemSelectionModel::Rows);
0931       }
0932 #if QT_VERSION >= 0x050600
0933       m_fileSelectionModel->setCurrentIndex(m_fileProxyModelFileIndexes.constFirst(),
0934                                             QItemSelectionModel::NoUpdate);
0935 #else
0936       m_fileSelectionModel->setCurrentIndex(m_fileProxyModelFileIndexes.first(),
0937                                             QItemSelectionModel::NoUpdate);
0938 #endif
0939     } else {
0940       m_fileSelectionModel->setCurrentIndex(m_fileProxyModelRootIndex,
0941           QItemSelectionModel::Clear | QItemSelectionModel::Current |
0942           QItemSelectionModel::Rows);
0943     }
0944   }
0945 
0946   emit directoryOpened();
0947 
0948   if (m_dirUpIndex.isValid()) {
0949     m_dirSelectionModel->setCurrentIndex(m_dirUpIndex,
0950         QItemSelectionModel::SelectCurrent | QItemSelectionModel::Rows);
0951     m_dirUpIndex = QPersistentModelIndex();
0952   }
0953 }
0954 
0955 /**
0956  * Called when the gatherer thread has finished to load the directory.
0957  */
0958 void Kid3Application::onDirectoryLoaded()
0959 {
0960   disconnect(m_fileProxyModel, &FileProxyModel::sortingFinished,
0961              this, &Kid3Application::onDirectoryLoaded);
0962   onDirectoryOpened();
0963 }
0964 
0965 /**
0966  * Unload all tags.
0967  * The tags of all files which are not modified or selected are freed to
0968  * reclaim their memory.
0969  */
0970 void Kid3Application::unloadAllTags()
0971 {
0972   TaggedFileIterator it(m_fileProxyModelRootIndex);
0973   while (it.hasNext()) {
0974     if (TaggedFile* taggedFile = it.next();
0975         taggedFile->isTagInformationRead() && !taggedFile->isChanged() &&
0976         !m_fileSelectionModel->isSelected(
0977           m_fileProxyModel->mapFromSource(taggedFile->getIndex()))) {
0978       taggedFile->clearTags(false);
0979       taggedFile->closeFileHandle();
0980     }
0981   }
0982 #if defined Q_OS_LINUX && defined __GLIBC__
0983   if (::malloc_trim(0)) {
0984     qDebug("Memory released by malloc_trim()");
0985   }
0986 #endif
0987 }
0988 
0989 /**
0990  * Get directory path of opened directory.
0991  * @return directory path.
0992  */
0993 QString Kid3Application::getDirPath() const
0994 {
0995   return FileProxyModel::getPathIfIndexOfDir(m_fileProxyModelRootIndex);
0996 }
0997 
0998 /**
0999  * Get current index in file proxy model or root index if current index is
1000  * invalid.
1001  * @return current index, root index if not valid.
1002  */
1003 QModelIndex Kid3Application::currentOrRootIndex() const
1004 {
1005   if (QModelIndex index(m_fileSelectionModel->currentIndex()); index.isValid())
1006     return index;
1007   return m_fileProxyModelRootIndex;
1008 }
1009 
1010 /**
1011  * Save all changed files.
1012  * longRunningOperationProgress() is emitted while saving files.
1013  *
1014  * @param errorDescriptions if not NULL, a list with error descriptions
1015  * corresponding to the errored files in the returned file list
1016  * is returned here. Null strings are used where no error description
1017  * is available.
1018  *
1019  * @return list of files with error, empty if ok.
1020  */
1021 QStringList Kid3Application::saveDirectory(QStringList* errorDescriptions)
1022 {
1023   QStringList errorFiles;
1024   int numFiles = 0, totalFiles = 0;
1025   // Get number of files to be saved to display correct progressbar
1026   TaggedFileIterator countIt(m_fileProxyModelRootIndex);
1027   while (countIt.hasNext()) {
1028     if (countIt.next()->isChanged()) {
1029       ++totalFiles;
1030     }
1031   }
1032   QString operationName = tr("Saving folder...");
1033   bool aborted = false;
1034   emit longRunningOperationProgress(operationName, -1, totalFiles, &aborted);
1035 
1036   if (errorDescriptions) {
1037     errorDescriptions->clear();
1038   }
1039   TaggedFileIterator it(m_fileProxyModelRootIndex);
1040   while (it.hasNext()) {
1041     TaggedFile* taggedFile = it.next();
1042     QString fileName = taggedFile->getFilename();
1043     if (taggedFile->isFilenameChanged() &&
1044         Utils::replaceIllegalFileNameCharacters(fileName)) {
1045       taggedFile->setFilename(fileName);
1046     }
1047     bool renamed = false;
1048     if (errorDescriptions) {
1049       errno = 0;
1050     }
1051     if (taggedFile->isChanged() &&
1052         !taggedFile->writeTags(false, &renamed,
1053                                FileConfig::instance().preserveTime())) {
1054       if (QDir dir(taggedFile->getDirname());
1055           dir.exists(fileName) && taggedFile->isFilenameChanged()) {
1056         // File is renamed to a file name which already exists.
1057         // Try another file name ending with a number.
1058         QString baseName = fileName;
1059         QString ext;
1060         if (int dotPos = baseName.lastIndexOf(QLatin1Char('.')); dotPos != -1) {
1061           ext = baseName.mid(dotPos);
1062           baseName.truncate(dotPos);
1063         }
1064         baseName.append(QLatin1Char('('));
1065         ext.prepend(QLatin1Char(')'));
1066         bool ok = false;
1067         for (int nr = 1; nr < 100; ++nr) {
1068           if (QString newName = baseName + QString::number(nr) + ext;
1069               !dir.exists(newName)) {
1070             taggedFile->setFilename(newName);
1071             ok = taggedFile->writeTags(false, &renamed,
1072                                        FileConfig::instance().preserveTime());
1073             break;
1074           }
1075         }
1076         if (ok) {
1077           continue;
1078         }
1079         taggedFile->setFilename(fileName);
1080       }
1081       QString errorMsg = taggedFile->getAbsFilename();
1082       errorFiles.push_back(errorMsg);
1083       if (errorDescriptions) {
1084         QString errorDescription;
1085         if (const int errnum = errno) {
1086           if (const char* errdesc = ::strerror(errnum)) {
1087             errorDescription = QString::fromUtf8(errdesc);
1088           }
1089         }
1090         errorDescriptions->append(errorDescription);
1091       }
1092     }
1093     ++numFiles;
1094     emit longRunningOperationProgress(operationName, numFiles, totalFiles,
1095                                       &aborted);
1096     if (aborted) {
1097       break;
1098     }
1099   }
1100   if (totalFiles == 0) {
1101     // To signal that operation is finished.
1102     ++totalFiles;
1103   }
1104   emit longRunningOperationProgress(operationName, totalFiles, totalFiles,
1105                                     &aborted);
1106 
1107   return errorFiles;
1108 }
1109 
1110 /**
1111  * Save all changed files.
1112  * longRunningOperationProgress() is emitted while saving files.
1113  *
1114  * @return list of files with error, empty if ok.
1115  */
1116 QStringList Kid3Application::saveDirectory()
1117 {
1118   return saveDirectory(nullptr);
1119 }
1120 
1121 /**
1122  * Merge entries of two string lists.
1123  *
1124  * Combine two string lists to a resulting list with all strings from
1125  * @a leftStrs having the corresponding string from @a rightStrs appended
1126  * if available. A @a separator can be given to join the two parts.
1127  * The @a rightStrs can contain fewer elements than @a leftStrs, the
1128  * resulting string will then be only the element from @a leftStrs.
1129  * This function can be used to add details to an error message, e.g.
1130  * mergeStringLists(errorMsgs, errorDescriptions, ": ").
1131  *
1132  * @param leftStrs strings for left part
1133  * @param rightStrs strings for right part
1134  * @param separator separator between left and right parts
1135  * @return string list with combined left and right parts.
1136  */
1137 QStringList Kid3Application::mergeStringLists(
1138     const QStringList& leftStrs, const QStringList& rightStrs,
1139     const QString& separator)
1140 {
1141   QStringList result;
1142   result.reserve(leftStrs.size());
1143   int i = 0;
1144   for (QString leftStr : leftStrs) {
1145     if (i < rightStrs.size()) {
1146       if (const QString& rightStr = rightStrs.at(i); !rightStr.isEmpty()) {
1147         leftStr += separator;
1148         leftStr += rightStr;
1149       }
1150     }
1151     result.append(leftStr);
1152     ++i;
1153   }
1154   return result;
1155 }
1156 
1157 /**
1158  * Update tags of selected files to contain contents of frame models.
1159  */
1160 void Kid3Application::frameModelsToTags()
1161 {
1162   if (!m_currentSelection.isEmpty()) {
1163     FOR_ALL_TAGS(tagNr) {
1164       FrameCollection frames(m_framesModel[tagNr]->getEnabledFrames());
1165       for (auto it = m_currentSelection.constBegin();
1166            it != m_currentSelection.constEnd();
1167            ++it) {
1168         if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(*it)) {
1169           taggedFile->setFrames(tagNr, frames);
1170         }
1171       }
1172     }
1173   }
1174 }
1175 
1176 /**
1177  * Update frame models to contain contents of selected files.
1178  * The properties starting with "selection" will be set by this method.
1179  */
1180 void Kid3Application::tagsToFrameModels()
1181 {
1182   QList<QPersistentModelIndex> indexes;
1183   const auto selectedIndexes = m_fileSelectionModel->selectedRows();
1184   indexes.reserve(selectedIndexes.size());
1185   for (const QModelIndex& index : selectedIndexes) {
1186     indexes.append(QPersistentModelIndex(index));
1187   }
1188 
1189   if (addTaggedFilesToSelection(indexes, true)) {
1190     m_currentSelection.swap(indexes);
1191   }
1192 }
1193 
1194 /**
1195  * Update frame models to contain contents of item selection.
1196  * The properties starting with "selection" will be set by this method.
1197  * @param selected item selection
1198  */
1199 void Kid3Application::selectedTagsToFrameModels(const QItemSelection& selected)
1200 {
1201   QList<QPersistentModelIndex> indexes;
1202   const auto selectedIndexes = selected.indexes();
1203   for (const QModelIndex& index : selectedIndexes) {
1204     if (index.column() == 0) {
1205       indexes.append(QPersistentModelIndex(index));
1206     }
1207   }
1208 
1209   if (addTaggedFilesToSelection(indexes, m_currentSelection.isEmpty())) {
1210     m_currentSelection.append(indexes);
1211   }
1212 }
1213 
1214 /**
1215  * Update frame models to contain contents of selected files.
1216  * @param indexes tagged file indexes
1217  * @param startSelection true if a new selection is started, false to add to
1218  * the existing selection
1219  * @return true if ok, false if selection operation is already running.
1220  */
1221 bool Kid3Application::addTaggedFilesToSelection(
1222     const QList<QPersistentModelIndex>& indexes, bool startSelection)
1223 {
1224   // It would crash if this is called while a long running selection operation
1225   // is in progress.
1226   if (m_selectionOperationRunning)
1227     return false;
1228 
1229   m_selectionOperationRunning = true;
1230 
1231   if (startSelection) {
1232     m_selection->beginAddTaggedFiles();
1233   }
1234 
1235   QElapsedTimer timer;
1236   timer.start();
1237   QString operationName = tr("Selection");
1238   int longRunningTotal = 0;
1239   int done = 0;
1240   bool aborted = false;
1241   for (auto it = indexes.constBegin(); it != indexes.constEnd(); ++it, ++done) {
1242     if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(*it)) {
1243       m_selection->addTaggedFile(taggedFile);
1244       if (!longRunningTotal) {
1245         if (timer.elapsed() >= 3000) {
1246           longRunningTotal = indexes.size();
1247           emit longRunningOperationProgress(operationName, -1, longRunningTotal,
1248                                             &aborted);
1249         }
1250       } else {
1251         emit longRunningOperationProgress(operationName, done, longRunningTotal,
1252                                           &aborted);
1253         if (aborted) {
1254           break;
1255         }
1256       }
1257     }
1258   }
1259   if (longRunningTotal) {
1260     emit longRunningOperationProgress(operationName, longRunningTotal,
1261                                       longRunningTotal, &aborted);
1262   }
1263 
1264   m_selection->endAddTaggedFiles();
1265 
1266   if (TaggedFile* taggedFile = m_selection->singleFile()) {
1267     FOR_ALL_TAGS(tagNr) {
1268       m_framelist[tagNr]->setTaggedFile(taggedFile);
1269     }
1270   }
1271   m_selection->clearUnusedFrames();
1272   m_selectionOperationRunning = false;
1273   return true;
1274 }
1275 
1276 /**
1277  * Revert file modifications.
1278  * Acts on selected files or all files if no file is selected.
1279  */
1280 void Kid3Application::revertFileModifications()
1281 {
1282   SelectedTaggedFileIterator it(getRootIndex(),
1283                                 getFileSelectionModel(),
1284                                 true);
1285   while (it.hasNext()) {
1286     TaggedFile* taggedFile = it.next();
1287     taggedFile->readTags(true);
1288   }
1289   if (!it.hasNoSelection()) {
1290     emit selectedFilesUpdated();
1291   }
1292 }
1293 
1294 /**
1295  * Set filter state.
1296  *
1297  * @param val true if list is filtered
1298  */
1299 void Kid3Application::setFiltered(bool val)
1300 {
1301   if (m_filtered != val) {
1302     m_filtered = val;
1303     emit filteredChanged(m_filtered);
1304   }
1305 }
1306 
1307 /**
1308  * Import.
1309  *
1310  * @param tagMask tag mask
1311  * @param path    path of file, "clipboard" for import from clipboard
1312  * @param fmtIdx  index of format
1313  *
1314  * @return true if ok.
1315  */
1316 bool Kid3Application::importTags(Frame::TagVersion tagMask,
1317                                  const QString& path, int fmtIdx)
1318 {
1319   const ImportConfig& importCfg = ImportConfig::instance();
1320   filesToTrackDataModel(importCfg.importDest());
1321   QString text;
1322   if (path == QLatin1String("clipboard")) {
1323     text = m_platformTools->readFromClipboard();
1324   } else {
1325     QFile file(path);
1326     if (file.open(QIODevice::ReadOnly)) {
1327       text = QTextStream(&file).readAll();
1328       file.close();
1329     }
1330   }
1331   if (!text.isNull() &&
1332       fmtIdx < importCfg.importFormatHeaders().size()) {
1333     TextImporter(getTrackDataModel()).updateTrackData(
1334       text,
1335       importCfg.importFormatHeaders().at(fmtIdx),
1336       importCfg.importFormatTracks().at(fmtIdx));
1337     trackDataModelToFiles(tagMask);
1338     return true;
1339   }
1340   return false;
1341 }
1342 
1343 /**
1344  * Import from tags.
1345  *
1346  * @param tagMask tag mask
1347  * @param source format to get source text from tags
1348  * @param extraction regular expression with frame names and captures to
1349  * extract from source text
1350  */
1351 void Kid3Application::importFromTags(Frame::TagVersion tagMask,
1352                                      const QString& source,
1353                                      const QString& extraction)
1354 {
1355   ImportTrackDataVector trackDataVector;
1356   filesToTrackData(tagMask, trackDataVector);
1357   TextImporter::importFromTags(source, extraction, trackDataVector);
1358   getTrackDataModel()->setTrackData(trackDataVector);
1359   trackDataModelToFiles(tagMask);
1360 }
1361 
1362 /**
1363  * Import from tags on selected files.
1364  *
1365  * @param tagMask tag mask
1366  * @param source format to get source text from tags
1367  * @param extraction regular expression with frame names and captures to
1368  * extract from source text
1369  *
1370  * @return extracted values for "%{__return}(.+)", empty if not used.
1371  */
1372 QStringList Kid3Application::importFromTagsToSelection(Frame::TagVersion tagMask,
1373                                                        const QString& source,
1374                                                        const QString& extraction)
1375 {
1376   emit fileSelectionUpdateRequested();
1377   SelectedTaggedFileIterator it(getRootIndex(),
1378                                 getFileSelectionModel(),
1379                                 true);
1380   ImportParser parser;
1381   parser.setFormat(extraction);
1382   while (it.hasNext()) {
1383     TaggedFile* taggedFile = it.next();
1384     taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
1385     ImportTrackData trackData(*taggedFile, tagMask);
1386     TextImporter::importFromTags(source, parser, trackData);
1387     taggedFile->setFrames(Frame::tagNumberFromMask(tagMask), trackData);
1388   }
1389   emit selectedFilesUpdated();
1390   return parser.getReturnValues();
1391 }
1392 
1393 /**
1394  * Export.
1395  *
1396  * @param tagVersion tag version
1397  * @param path   path of file, "clipboard" for export to clipboard
1398  * @param fmtIdx index of format
1399  *
1400  * @return true if ok.
1401  */
1402 bool Kid3Application::exportTags(Frame::TagVersion tagVersion,
1403                                  const QString& path, int fmtIdx)
1404 {
1405   ImportTrackDataVector trackDataVector;
1406   filesToTrackData(tagVersion, trackDataVector);
1407   m_textExporter->setTrackData(trackDataVector);
1408   m_textExporter->updateTextUsingConfig(fmtIdx);
1409   if (path == QLatin1String("clipboard")) {
1410     return m_platformTools->writeToClipboard(m_textExporter->getText());
1411   }
1412   return m_textExporter->exportToFile(path);
1413 }
1414 
1415 /**
1416  * Write playlist according to playlist configuration.
1417  *
1418  * @param cfg playlist configuration to use
1419  *
1420  * @return true if ok.
1421  */
1422 bool Kid3Application::writePlaylist(const PlaylistConfig& cfg)
1423 {
1424   PlaylistCreator plCtr(getDirPath(), cfg);
1425   QItemSelectionModel* selectModel = getFileSelectionModel();
1426   bool noSelection = !cfg.onlySelectedFiles() || !selectModel ||
1427                      !selectModel->hasSelection();
1428   bool ok = true;
1429   QModelIndex rootIndex;
1430 
1431   if (cfg.location() == PlaylistConfig::PL_CurrentDirectory) {
1432     // Get first child of parent of current index.
1433     rootIndex = currentOrRootIndex();
1434     if (rootIndex.model() && rootIndex.model()->rowCount(rootIndex) <= 0)
1435       rootIndex = rootIndex.parent();
1436     if (const QAbstractItemModel* model = rootIndex.model()) {
1437       for (int row = 0; row < model->rowCount(rootIndex); ++row) {
1438         QModelIndex index = model->index(row, 0, rootIndex);
1439         if (PlaylistCreator::Item plItem(index, plCtr);
1440             plItem.isFile() &&
1441             (noSelection || selectModel->isSelected(index))) {
1442           ok = plItem.add() && ok;
1443         }
1444       }
1445     }
1446   } else {
1447     QString selectedDirPrefix;
1448     rootIndex = getRootIndex();
1449     ModelIterator it(rootIndex);
1450     while (it.hasNext()) {
1451       QModelIndex index = it.next();
1452       PlaylistCreator::Item plItem(index, plCtr);
1453       bool inSelectedDir = false;
1454       if (plItem.isDir()) {
1455         if (!selectedDirPrefix.isEmpty()) {
1456           if (plItem.getDirName().startsWith(selectedDirPrefix)) {
1457             inSelectedDir = true;
1458           } else {
1459             selectedDirPrefix = QLatin1String("");
1460           }
1461         }
1462         if (inSelectedDir || noSelection || selectModel->isSelected(index)) {
1463           // if directory is selected, all its files are selected
1464           if (!inSelectedDir) {
1465             selectedDirPrefix = plItem.getDirName();
1466           }
1467         }
1468       } else if (plItem.isFile()) {
1469         QString dirName = plItem.getDirName();
1470         if (!selectedDirPrefix.isEmpty()) {
1471           if (dirName.startsWith(selectedDirPrefix)) {
1472             inSelectedDir = true;
1473           } else {
1474             selectedDirPrefix = QLatin1String("");
1475           }
1476         }
1477         if (inSelectedDir || noSelection || selectModel->isSelected(index)) {
1478           ok = plItem.add() && ok;
1479         }
1480       }
1481     }
1482   }
1483 
1484   ok = plCtr.write() && ok;
1485   return ok;
1486 }
1487 
1488 /**
1489  * Write empty playlist.
1490  * @param cfg playlist configuration to use
1491  * @param fileName file name for playlist
1492  * @return true if ok.
1493  */
1494 bool Kid3Application::writeEmptyPlaylist(const PlaylistConfig& cfg,
1495                                          const QString& fileName)
1496 {
1497   QString path = getDirPath();
1498   PlaylistCreator plCtr(path, cfg);
1499   if (!path.endsWith(QLatin1Char('/'))) {
1500     path += QLatin1Char('/');
1501   }
1502   path += fileName;
1503   if (QString ext = cfg.fileExtensionForFormat(); !path.endsWith(ext)) {
1504     path += ext;
1505   }
1506   return plCtr.write(path, QList<QPersistentModelIndex>());
1507 }
1508 
1509 /**
1510  * Write playlist using current playlist configuration.
1511  *
1512  * @return true if ok.
1513  */
1514 bool Kid3Application::writePlaylist()
1515 {
1516   return writePlaylist(PlaylistConfig::instance());
1517 }
1518 
1519 /**
1520  * Get items of a playlist.
1521  * @param path path to playlist file
1522  * @return list of absolute paths to playlist items.
1523  */
1524 QStringList Kid3Application::getPlaylistItems(const QString& path)
1525 {
1526   return playlistModel(path)->pathsInPlaylist();
1527 }
1528 
1529 /**
1530  * Set items of a playlist.
1531  * @param path path to playlist file
1532  * @param items list of absolute paths to playlist items
1533  * @return true if ok, false if not all @a items were found and added or
1534  *         saving failed.
1535  */
1536 bool Kid3Application::setPlaylistItems(const QString& path,
1537                                        const QStringList& items)
1538 {
1539   if (PlaylistModel* model = playlistModel(path);
1540       model->setPathsInPlaylist(items)) {
1541     return model->save();
1542   }
1543   return false;
1544 }
1545 
1546 /**
1547  * Get playlist model for a play list file.
1548  * @param path path to playlist file
1549  * @return playlist model.
1550  */
1551 PlaylistModel* Kid3Application::playlistModel(const QString& path)
1552 {
1553   // Create an absolute path with a value which does not depend on the file's
1554   // existence or whether the path given is relative or absolute.
1555   QString absPath;
1556   if (!path.isEmpty()) {
1557     QFileInfo fileInfo(path);
1558     absPath = fileInfo.absoluteDir().filePath(fileInfo.fileName());
1559   }
1560 
1561   PlaylistModel* model = m_playlistModels.value(absPath);
1562   if (!model) {
1563     model = new PlaylistModel(m_fileProxyModel, this);
1564     m_playlistModels.insert(absPath, model);
1565   }
1566   model->setPlaylistFile(absPath);
1567   return model;
1568 }
1569 
1570 /**
1571  * Check if any playlist model has unsaved modifications.
1572  * @return true if there is a modified playlist model.
1573  */
1574 bool Kid3Application::hasModifiedPlaylistModel() const
1575 {
1576   for (auto it = m_playlistModels.constBegin();
1577        it != m_playlistModels.constEnd();
1578        ++it) {
1579     if ((*it)->isModified()) {
1580       return true;
1581     }
1582   }
1583   return false;
1584 }
1585 
1586 /**
1587  * Save all modified playlist models.
1588  */
1589 void Kid3Application::saveModifiedPlaylistModels()
1590 {
1591   for (auto it = m_playlistModels.begin(); it != m_playlistModels.end(); ++it) { // clazy:exclude=detaching-member
1592     if ((*it)->isModified()) {
1593       (*it)->save();
1594     }
1595   }
1596 }
1597 
1598 /**
1599  * Set track data with tagged files of directory.
1600  *
1601  * @param tagVersion tag version
1602  * @param trackDataList is filled with track data
1603  */
1604 void Kid3Application::filesToTrackData(Frame::TagVersion tagVersion,
1605                                        ImportTrackDataVector& trackDataList)
1606 {
1607   TaggedFileOfDirectoryIterator it(currentOrRootIndex());
1608   while (it.hasNext()) {
1609     TaggedFile* taggedFile = it.next();
1610     taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
1611     trackDataList.push_back(ImportTrackData(*taggedFile, tagVersion));
1612   }
1613 }
1614 
1615 /**
1616  * Set track data model with tagged files of directory.
1617  *
1618  * @param tagVersion tag version
1619  */
1620 void Kid3Application::filesToTrackDataModel(Frame::TagVersion tagVersion)
1621 {
1622   ImportTrackDataVector trackDataList;
1623   filesToTrackData(tagVersion, trackDataList);
1624   getTrackDataModel()->setTrackData(trackDataList);
1625 }
1626 
1627 /**
1628  * Set tagged files of directory from track data model.
1629  *
1630  * @param tagVersion tags to set
1631  */
1632 void Kid3Application::trackDataModelToFiles(Frame::TagVersion tagVersion)
1633 {
1634   ImportTrackDataVector trackDataList(getTrackDataModel()->getTrackData());
1635   auto it = trackDataList.begin();
1636   FrameFilter flt;
1637   if (Frame::TagNumber fltTagNr = Frame::tagNumberFromMask(tagVersion);
1638       fltTagNr < Frame::Tag_NumValues) {
1639     flt = frameModel(fltTagNr)->getEnabledFrameFilter(true);
1640   }
1641   TaggedFileOfDirectoryIterator tfit(currentOrRootIndex());
1642   while (tfit.hasNext()) {
1643     TaggedFile* taggedFile = tfit.next();
1644     taggedFile->readTags(false);
1645     if (it != trackDataList.end()) {
1646       it->removeDisabledFrames(flt);
1647       formatFramesIfEnabled(*it);
1648       FOR_TAGS_IN_MASK(tagNr, tagVersion) {
1649         if (tagNr == Frame::Tag_Id3v1) {
1650           taggedFile->setFrames(tagNr, *it, false);
1651         } else {
1652           FrameCollection oldFrames;
1653           taggedFile->getAllFrames(tagNr, oldFrames);
1654           it->markChangedFrames(oldFrames);
1655           taggedFile->setFrames(tagNr, *it, true);
1656         }
1657       }
1658       ++it;
1659     } else {
1660       break;
1661     }
1662   }
1663 
1664   if ((tagVersion & (1 << Frame::Tag_Picture)) &&
1665       flt.isEnabled(Frame::FT_Picture) &&
1666       !trackDataList.getCoverArtUrl().isEmpty()) {
1667     downloadImage(trackDataList.getCoverArtUrl(), ImageForImportTrackData);
1668   }
1669 
1670   if (getFileSelectionModel()->hasSelection()) {
1671     emit selectedFilesUpdated();
1672   }
1673 }
1674 
1675 /**
1676  * Download an image file.
1677  *
1678  * @param url  URL of image
1679  * @param dest specifies affected files
1680  */
1681 void Kid3Application::downloadImage(const QUrl& url, DownloadImageDestination dest)
1682 {
1683   if (QUrl imgurl(DownloadClient::getImageUrl(url)); !imgurl.isEmpty()) {
1684     m_downloadImageDest = dest;
1685     m_downloadClient->startDownload(imgurl);
1686   }
1687 }
1688 
1689 /**
1690  * Download an image file.
1691  *
1692  * @param url URL of image
1693  * @param allFilesInDir true to add the image to all files in the directory
1694  */
1695 void Kid3Application::downloadImage(const QString& url, bool allFilesInDir)
1696 {
1697   QUrl imgurl(url);
1698   downloadImage(imgurl, allFilesInDir
1699                 ? ImageForAllFilesInDirectory : ImageForSelectedFiles);
1700 }
1701 
1702 /**
1703  * Perform a batch import for the selected directories.
1704  * @param profile batch import profile
1705  * @param tagVersion import destination tag versions
1706  */
1707 void Kid3Application::batchImport(const BatchImportProfile& profile,
1708                                   Frame::TagVersion tagVersion)
1709 {
1710   m_batchImportProfile = &profile;
1711   m_batchImportTagVersion = tagVersion;
1712   m_batchImportAlbums.clear();
1713   m_batchImportTrackDataList.clear();
1714   m_lastProcessedDirName.clear();
1715   m_batchImporter->clearAborted();
1716   m_batchImporter->emitReportImportEvent(BatchImporter::ReadingDirectory,
1717                                          QString());
1718   // If no directories are selected, process files of the current directory.
1719   QList<QPersistentModelIndex> indexes;
1720   const auto selectedIndexes = m_fileSelectionModel->selectedRows();
1721   for (const QModelIndex& index : selectedIndexes) {
1722     if (m_fileProxyModel->isDir(index)) {
1723       indexes.append(index);
1724     }
1725   }
1726   if (indexes.isEmpty()) {
1727     indexes.append(m_fileProxyModelRootIndex);
1728   }
1729 
1730   connect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady,
1731           this, &Kid3Application::batchImportNextFile);
1732   m_fileProxyModelIterator->start(indexes);
1733 }
1734 
1735 /**
1736  * Perform a batch import for the selected directories.
1737  * @param profileName batch import profile name
1738  * @param tagVersion import destination tag versions
1739  * @return true if profile with @a profileName found.
1740  */
1741 bool Kid3Application::batchImport(const QString& profileName,
1742                                   Frame::TagVersion tagVersion)
1743 {
1744   if (!m_namedBatchImportProfile) {
1745     m_namedBatchImportProfile.reset(new BatchImportProfile);
1746   }
1747   if (BatchImportConfig::instance().getProfileByName(
1748         profileName, *m_namedBatchImportProfile)) {
1749     batchImport(*m_namedBatchImportProfile, tagVersion);
1750     return true;
1751   }
1752   return false;
1753 }
1754 
1755 /**
1756  * Apply single file to batch import.
1757  *
1758  * @param index index of file in file proxy model
1759  */
1760 void Kid3Application::batchImportNextFile(const QPersistentModelIndex& index)
1761 {
1762   bool terminated = !index.isValid();
1763   if (!terminated) {
1764     if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
1765       taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
1766       if (taggedFile->getDirname() != m_lastProcessedDirName) {
1767         m_lastProcessedDirName = taggedFile->getDirname();
1768         if (!m_batchImportTrackDataList.isEmpty()) {
1769           m_batchImportAlbums.append(m_batchImportTrackDataList);
1770         }
1771         m_batchImportTrackDataList.clear();
1772         if (m_batchImporter->isAborted()) {
1773           terminated = true;
1774         }
1775       }
1776       m_batchImportTrackDataList.append(ImportTrackData(*taggedFile,
1777                                                       m_batchImportTagVersion));
1778     }
1779   }
1780   if (terminated) {
1781     m_fileProxyModelIterator->abort();
1782     disconnect(m_fileProxyModelIterator,
1783                &FileProxyModelIterator::nextReady,
1784                this, &Kid3Application::batchImportNextFile);
1785     if (!m_batchImporter->isAborted()) {
1786       if (!m_batchImportTrackDataList.isEmpty()) {
1787         m_batchImportAlbums.append(m_batchImportTrackDataList);
1788       }
1789       if (Frame::TagNumber fltTagNr =
1790             Frame::tagNumberFromMask(m_batchImportTagVersion);
1791           fltTagNr < Frame::Tag_NumValues) {
1792         m_batchImporter->setFrameFilter(
1793               frameModel(fltTagNr)->getEnabledFrameFilter(true));
1794       }
1795       m_batchImporter->start(m_batchImportAlbums, *m_batchImportProfile,
1796                              m_batchImportTagVersion);
1797     }
1798   }
1799 }
1800 
1801 /**
1802  * Format frames if format while editing is switched on.
1803  *
1804  * @param frames frames
1805  */
1806 void Kid3Application::formatFramesIfEnabled(FrameCollection& frames) const
1807 {
1808   TagFormatConfig::instance().formatFramesIfEnabled(frames);
1809 }
1810 
1811 /**
1812  * Get name of selected file.
1813  *
1814  * @return absolute file name, ends with "/" if it is a directory.
1815  */
1816 QString Kid3Application::getFileNameOfSelectedFile()
1817 {
1818   QModelIndex index = getFileSelectionModel()->currentIndex();
1819   if (QString dirname = FileProxyModel::getPathIfIndexOfDir(index);
1820       !dirname.isNull()) {
1821     if (!dirname.endsWith(QLatin1Char('/'))) dirname += QLatin1Char('/');
1822     return dirname;
1823   }
1824   if (TaggedFile* taggedFile =
1825     FileProxyModel::getTaggedFileOfIndex(index)) {
1826     return taggedFile->getAbsFilename();
1827   }
1828   return QLatin1String("");
1829 }
1830 
1831 /**
1832  * Set name of selected file.
1833  * Exactly one file has to be selected.
1834  *
1835  * @param name file name.
1836  */
1837 void Kid3Application::setFileNameOfSelectedFile(const QString& name)
1838 {
1839   if (TaggedFile* taggedFile = getSelectedFile()) {
1840     QFileInfo fi(name);
1841     taggedFile->setFilename(fi.fileName());
1842     emit selectedFilesUpdated();
1843   }
1844 }
1845 
1846 /**
1847  * Apply filename format.
1848  */
1849 void Kid3Application::applyFilenameFormat()
1850 {
1851   emit fileSelectionUpdateRequested();
1852   SelectedTaggedFileIterator it(getRootIndex(),
1853                                 getFileSelectionModel(),
1854                                 true);
1855   while (it.hasNext()) {
1856     TaggedFile* taggedFile = it.next();
1857     taggedFile->readTags(false);
1858     QString fn = taggedFile->getFilename();
1859     FilenameFormatConfig::instance().formatString(fn);
1860     taggedFile->setFilename(fn);
1861   }
1862   emit selectedFilesUpdated();
1863 }
1864 
1865 /**
1866  * Apply tag format.
1867  */
1868 void Kid3Application::applyTagFormat()
1869 {
1870   emit fileSelectionUpdateRequested();
1871   FrameCollection frames;
1872   FrameFilter flt[Frame::Tag_NumValues];
1873   FOR_ALL_TAGS(tagNr) {
1874     flt[tagNr] = frameModel(tagNr)->getEnabledFrameFilter(true);
1875   }
1876   SelectedTaggedFileIterator it(getRootIndex(),
1877                                 getFileSelectionModel(),
1878                                 true);
1879   while (it.hasNext()) {
1880     TaggedFile* taggedFile = it.next();
1881     taggedFile->readTags(false);
1882     FOR_ALL_TAGS(tagNr) {
1883       taggedFile->getAllFrames(tagNr, frames);
1884       frames.removeDisabledFrames(flt[tagNr]);
1885       TagFormatConfig::instance().formatFrames(frames);
1886       taggedFile->setFrames(tagNr, frames);
1887     }
1888   }
1889   emit selectedFilesUpdated();
1890 }
1891 
1892 /**
1893  * Apply text encoding.
1894  * Set the text encoding selected in the settings Tags/ID3v2/Text encoding
1895  * for all selected files which have an ID3v2 tag.
1896  */
1897 void Kid3Application::applyTextEncoding()
1898 {
1899   emit fileSelectionUpdateRequested();
1900   Frame::TextEncoding encoding = frameTextEncodingFromConfig();
1901   FrameCollection frames;
1902   SelectedTaggedFileIterator it(getRootIndex(),
1903                                 getFileSelectionModel(),
1904                                 true);
1905   while (it.hasNext()) {
1906     TaggedFile* taggedFile = it.next();
1907     taggedFile->readTags(false);
1908     taggedFile->getAllFrames(Frame::Tag_Id3v2, frames);
1909     for (auto frameIt = frames.begin(); frameIt != frames.end(); ++frameIt) {
1910       auto& frame = const_cast<Frame&>(*frameIt);
1911       Frame::TextEncoding enc = encoding;
1912       if (taggedFile->getTagFormat(Frame::Tag_Id3v2) == QLatin1String("ID3v2.3.0")) {
1913         // TagLib sets the ID3v2.3.0 frame containing the date internally with
1914         // ISO-8859-1, so the encoding cannot be set for such frames.
1915         if (taggedFile->taggedFileKey() == QLatin1String("TaglibMetadata") &&
1916             frame.getType() == Frame::FT_Date &&
1917             enc != Frame::TE_ISO8859_1)
1918           continue;
1919         // Only ISO-8859-1 and UTF16 are allowed for ID3v2.3.0.
1920         if (enc != Frame::TE_ISO8859_1)
1921           enc = Frame::TE_UTF16;
1922       }
1923       Frame::FieldList& fields = frame.fieldList();
1924       for (auto fieldIt = fields.begin(); fieldIt != fields.end(); ++fieldIt) {
1925         if (fieldIt->m_id == Frame::ID_TextEnc &&
1926             fieldIt->m_value.toInt() != enc) {
1927           fieldIt->m_value = enc;
1928           frame.setValueChanged();
1929         }
1930       }
1931     }
1932     taggedFile->setFrames(Frame::Tag_Id3v2, frames);
1933   }
1934   emit selectedFilesUpdated();
1935 }
1936 
1937 /**
1938  * Copy tags into copy buffer.
1939  *
1940  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
1941  */
1942 void Kid3Application::copyTags(Frame::TagVersion tagMask)
1943 {
1944   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
1945   if (tagNr >= Frame::Tag_NumValues)
1946     return;
1947 
1948   emit fileSelectionUpdateRequested();
1949   m_copyTags = frameModel(tagNr)->frames().copyEnabledFrames(
1950     frameModel(tagNr)->getEnabledFrameFilter(true));
1951 }
1952 
1953 /**
1954  * Paste from copy buffer to tags.
1955  *
1956  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
1957  */
1958 void Kid3Application::pasteTags(Frame::TagVersion tagMask)
1959 {
1960   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
1961   if (tagNr >= Frame::Tag_NumValues)
1962     return;
1963 
1964   emit fileSelectionUpdateRequested();
1965   FrameCollection frames(m_copyTags.copyEnabledFrames(
1966                          frameModel(tagNr)->getEnabledFrameFilter(true)));
1967   formatFramesIfEnabled(frames);
1968   SelectedTaggedFileIterator it(getRootIndex(),
1969                                 getFileSelectionModel(),
1970                                 false);
1971   while (it.hasNext()) {
1972     it.next()->setFrames(tagNr, frames, false);
1973   }
1974   emit selectedFilesUpdated();
1975 }
1976 
1977 /**
1978  * Set tag from other tag.
1979  *
1980  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
1981  */
1982 void Kid3Application::copyToOtherTag(Frame::TagVersion tagMask)
1983 {
1984   Frame::TagNumber dstTagNr = Frame::tagNumberFromMask(tagMask);
1985   if (dstTagNr >= Frame::Tag_NumValues)
1986     return;
1987 
1988   Frame::TagNumber srcTagNr = dstTagNr == Frame::Tag_2
1989       ? Frame::Tag_1 : Frame::Tag_2;
1990   copyTag(srcTagNr, dstTagNr);
1991 }
1992 
1993 /**
1994  * Copy from a tag to another tag.
1995  * @param srcTagNr source tag number
1996  * @param dstTagNr destination tag number
1997  */
1998 void Kid3Application::copyTag(Frame::TagNumber srcTagNr, Frame::TagNumber dstTagNr)
1999 {
2000   emit fileSelectionUpdateRequested();
2001   FrameCollection frames;
2002   FrameFilter flt(frameModel(dstTagNr)->getEnabledFrameFilter(true));
2003   SelectedTaggedFileIterator it(getRootIndex(),
2004                                 getFileSelectionModel(),
2005                                 false);
2006   while (it.hasNext()) {
2007     TaggedFile* taggedFile = it.next();
2008     taggedFile->getAllFrames(srcTagNr, frames);
2009     frames.removeDisabledFrames(flt);
2010     frames.setIndexesInvalid();
2011     formatFramesIfEnabled(frames);
2012     taggedFile->setFrames(dstTagNr, frames, false);
2013   }
2014   emit selectedFilesUpdated();
2015 }
2016 
2017 /**
2018  * Remove tags in selected files.
2019  *
2020  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
2021  */
2022 void Kid3Application::removeTags(Frame::TagVersion tagMask)
2023 {
2024   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
2025   if (tagNr >= Frame::Tag_NumValues)
2026     return;
2027 
2028   emit fileSelectionUpdateRequested();
2029   FrameFilter flt(frameModel(tagNr)->getEnabledFrameFilter(true));
2030   SelectedTaggedFileIterator it(getRootIndex(),
2031                                 getFileSelectionModel(),
2032                                 false);
2033   while (it.hasNext()) {
2034     it.next()->deleteFrames(tagNr, flt);
2035   }
2036   emit selectedFilesUpdated();
2037 }
2038 
2039 /**
2040  * Set tags according to filename.
2041  *
2042  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
2043  */
2044 void Kid3Application::getTagsFromFilename(Frame::TagVersion tagMask)
2045 {
2046   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
2047   if (tagNr >= Frame::Tag_NumValues)
2048     return;
2049 
2050   emit fileSelectionUpdateRequested();
2051   FrameCollection frames;
2052   QItemSelectionModel* selectModel = getFileSelectionModel();
2053   SelectedTaggedFileIterator it(getRootIndex(),
2054                                 selectModel,
2055                                 false);
2056   FrameFilter flt(frameModel(tagNr)->getEnabledFrameFilter(true));
2057   while (it.hasNext()) {
2058     TaggedFile* taggedFile = it.next();
2059     taggedFile->getAllFrames(tagNr, frames);
2060     taggedFile->getTagsFromFilename(
2061           frames, FileConfig::instance().fromFilenameFormat());
2062     frames.removeDisabledFrames(flt);
2063     formatFramesIfEnabled(frames);
2064     taggedFile->setFrames(tagNr, frames);
2065   }
2066   emit selectedFilesUpdated();
2067 }
2068 
2069 /**
2070  * Set filename according to tags.
2071  * If a single file is selected the tags in the GUI controls
2072  * are used, else the tags in the multiple selected files.
2073  *
2074  * @param tagVersion tag version
2075  */
2076 void Kid3Application::getFilenameFromTags(Frame::TagVersion tagVersion)
2077 {
2078   emit fileSelectionUpdateRequested();
2079   QItemSelectionModel* selectModel = getFileSelectionModel();
2080   SelectedTaggedFileIterator it(getRootIndex(),
2081                                 selectModel,
2082                                 false);
2083   while (it.hasNext()) {
2084     TaggedFile* taggedFile = it.next();
2085     if (TrackData trackData(*taggedFile, tagVersion);
2086         !trackData.isEmptyOrInactive()) {
2087       taggedFile->setFilenameFormattedIfEnabled(
2088         trackData.formatFilenameFromTags(FileConfig::instance().toFilenameFormat()));
2089     }
2090   }
2091   emit selectedFilesUpdated();
2092 }
2093 
2094 /**
2095  * Get the selected file.
2096  *
2097  * @return the selected file,
2098  *         0 if not exactly one file is selected
2099  */
2100 TaggedFile* Kid3Application::getSelectedFile()
2101 {
2102   QModelIndexList selItems(
2103       m_fileSelectionModel->selectedRows());
2104   if (selItems.size() != 1)
2105     return nullptr;
2106 
2107   return FileProxyModel::getTaggedFileOfIndex(selItems.first());
2108 }
2109 
2110 /**
2111  * Update the stored current selection with the list of all selected items.
2112  */
2113 void Kid3Application::updateCurrentSelection()
2114 {
2115   m_currentSelection.clear();
2116   const auto selectedIndexes = m_fileSelectionModel->selectedRows();
2117   for (const QModelIndex& index : selectedIndexes) {
2118     m_currentSelection.append(QPersistentModelIndex(index));
2119   }
2120 }
2121 
2122 /**
2123  * Edit selected frame.
2124  * @param tagNr tag number
2125  */
2126 void Kid3Application::editFrame(Frame::TagNumber tagNr)
2127 {
2128   FrameList* framelist = m_framelist[tagNr];
2129   emit fileSelectionUpdateRequested();
2130   m_editFrameTaggedFile = getSelectedFile();
2131   if (const Frame* selectedFrame = frameModel(tagNr)->getFrameOfIndex(
2132         getFramesSelectionModel(tagNr)->currentIndex())) {
2133     if (m_editFrameTaggedFile) {
2134       framelist->setTaggedFile(m_editFrameTaggedFile);
2135       framelist->setFrame(*selectedFrame);
2136       if (selectedFrame->getIndex() != -1) {
2137         framelist->editFrame();
2138       } else {
2139         // Edit a frame which does not exist, switch to add mode.
2140         m_addFrameTaggedFile = m_editFrameTaggedFile;
2141         m_editFrameTaggedFile = nullptr;
2142         framelist->addAndEditFrame();
2143       }
2144     } else {
2145       // multiple files selected
2146       // Get the first selected file by using a temporary iterator.
2147       if (TaggedFile* firstFile = SelectedTaggedFileIterator(
2148             getRootIndex(), getFileSelectionModel(), false).peekNext()) {
2149         framelist->setTaggedFile(firstFile);
2150         m_editFrameName = framelist->getSelectedName();
2151         if (!m_editFrameName.isEmpty()) {
2152           framelist->setFrame(*selectedFrame);
2153           framelist->addFrameFieldList();
2154           framelist->editFrame();
2155         }
2156       }
2157     }
2158   }
2159 }
2160 
2161 /**
2162  * Called when a frame is edited.
2163  * @param frame edited frame, 0 if canceled
2164  */
2165 void Kid3Application::onFrameEdited(const Frame* frame)
2166 {
2167   auto framelist = qobject_cast<FrameList*>(sender());
2168   if (!framelist || !frame)
2169     return;
2170 
2171   Frame::TagNumber tagNr = framelist->tagNumber();
2172   if (m_editFrameTaggedFile) {
2173     emit frameModified(m_editFrameTaggedFile, tagNr);
2174   } else {
2175     framelist->setFrame(*frame);
2176 
2177     // Start a new iteration because the file selection model can be
2178     // changed by editFrameOfTaggedFile(), e.g. when a file is exported
2179     // from a picture frame.
2180     SelectedTaggedFileIterator tfit(getRootIndex(),
2181                                     getFileSelectionModel(),
2182                                     false);
2183     while (tfit.hasNext()) {
2184       TaggedFile* currentFile = tfit.next();
2185       FrameCollection frames;
2186       currentFile->getAllFrames(tagNr, frames);
2187       for (auto it = frames.cbegin(); it != frames.cend(); ++it) {
2188         if (it->getName() == m_editFrameName) {
2189           currentFile->deleteFrame(tagNr, *it);
2190           break;
2191         }
2192       }
2193       framelist->setTaggedFile(currentFile);
2194       framelist->pasteFrame();
2195     }
2196     emit selectedFilesUpdated();
2197     framelist->selectByName(frame->getName());
2198   }
2199 }
2200 
2201 /**
2202  * Delete selected frame.
2203  * @param tagNr tag number
2204  * @param frameName name of frame to delete, empty to delete selected frame
2205  * @param index 0 for first frame with @a frameName, 1 for second, etc.
2206  */
2207 void Kid3Application::deleteFrame(Frame::TagNumber tagNr,
2208                                   const QString& frameName, int index)
2209 {
2210   FrameList* framelist = m_framelist[tagNr];
2211   emit fileSelectionUpdateRequested();
2212   if (TaggedFile* taggedFile = getSelectedFile();
2213       taggedFile && frameName.isEmpty()) {
2214     // delete selected frame from single file
2215     if (!framelist->deleteFrame()) {
2216       // frame not found
2217       return;
2218     }
2219     emit frameModified(taggedFile, tagNr);
2220   } else {
2221     // multiple files selected or frame name specified
2222     bool firstFile = true;
2223     QString name;
2224     SelectedTaggedFileIterator tfit(getRootIndex(),
2225                                     getFileSelectionModel(),
2226                                     false);
2227     while (tfit.hasNext()) {
2228       TaggedFile* currentFile = tfit.next();
2229       if (firstFile) {
2230         firstFile = false;
2231         taggedFile = currentFile;
2232         framelist->setTaggedFile(taggedFile);
2233         name = frameName.isEmpty() ? framelist->getSelectedName() : frameName;
2234       }
2235       FrameCollection frames;
2236       currentFile->getAllFrames(tagNr, frames);
2237       int currentIndex = 0;
2238       for (auto it = frames.cbegin(); it != frames.cend(); ++it) {
2239         if (it->getName() == name) {
2240           if (currentIndex == index) {
2241             currentFile->deleteFrame(tagNr, *it);
2242             break;
2243           }
2244           ++currentIndex;
2245         }
2246       }
2247     }
2248     framelist->saveCursor();
2249     emit selectedFilesUpdated();
2250     framelist->restoreCursor();
2251   }
2252 }
2253 
2254 /**
2255  * Select a frame type and add such a frame to frame list.
2256  * @param tagNr tag number
2257  * @param frame frame to add, if 0 the user has to select and edit the frame
2258  * @param edit if true and a frame is set, the user can edit the frame before
2259  * it is added
2260  */
2261 void Kid3Application::addFrame(Frame::TagNumber tagNr,
2262                                const Frame* frame, bool edit)
2263 {
2264   if (tagNr >= Frame::Tag_NumValues)
2265     return;
2266 
2267   FrameList* framelist = m_framelist[tagNr];
2268   emit fileSelectionUpdateRequested();
2269   TaggedFile* currentFile = nullptr;
2270   m_addFrameTaggedFile = getSelectedFile();
2271   if (m_addFrameTaggedFile) {
2272     currentFile = m_addFrameTaggedFile;
2273   } else {
2274     // multiple files selected
2275     if (SelectedTaggedFileIterator tfit(
2276           getRootIndex(), getFileSelectionModel(), false);
2277         tfit.hasNext()) {
2278       currentFile = tfit.next();
2279       framelist->setTaggedFile(currentFile);
2280     }
2281   }
2282 
2283   if (currentFile) {
2284     if (edit) {
2285       if (frame) {
2286         framelist->setFrame(*frame);
2287         framelist->addAndEditFrame();
2288       } else {
2289         framelist->selectAddAndEditFrame();
2290       }
2291     } else {
2292       framelist->setFrame(*frame);
2293       onFrameAdded(framelist->pasteFrame() ? &framelist->getFrame()
2294                                            : nullptr, tagNr);
2295     }
2296   }
2297 }
2298 
2299 /**
2300  * Called when a frame is added.
2301  * @param frame edited frame, 0 if canceled
2302  * @param tagNr tag number used if slot is not invoked by framelist signal
2303  */
2304 void Kid3Application::onFrameAdded(const Frame* frame, Frame::TagNumber tagNr)
2305 {
2306   if (!frame)
2307     return;
2308 
2309   auto framelist = qobject_cast<FrameList*>(sender());
2310   if (!framelist) {
2311     framelist = m_framelist[tagNr];
2312   }
2313   if (m_addFrameTaggedFile) {
2314     emit frameModified(m_addFrameTaggedFile, tagNr);
2315     if (framelist->isPictureFrame()) {
2316       // update preview picture
2317       emit selectedFilesUpdated();
2318     }
2319   } else {
2320     // multiple files selected
2321     bool firstFile = true;
2322     int frameId = -1;
2323     framelist->setFrame(*frame);
2324 
2325     SelectedTaggedFileIterator tfit(getRootIndex(),
2326                                     getFileSelectionModel(),
2327                                     false);
2328     while (tfit.hasNext()) {
2329       TaggedFile* currentFile = tfit.next();
2330       if (firstFile) {
2331         firstFile = false;
2332         m_addFrameTaggedFile = currentFile;
2333         framelist->setTaggedFile(currentFile);
2334         frameId = framelist->getSelectedId();
2335       } else {
2336         framelist->setTaggedFile(currentFile);
2337         framelist->pasteFrame();
2338       }
2339     }
2340     framelist->setTaggedFile(m_addFrameTaggedFile);
2341     if (frameId != -1) {
2342       framelist->setSelectedId(frameId);
2343     }
2344     emit selectedFilesUpdated();
2345     framelist->selectByName(frame->getName());
2346   }
2347 }
2348 
2349 /**
2350  * Called by framelist when a frame is added.
2351  * Same as onFrameAdded() with default argument, provided for functor-based
2352  * connections.
2353  * @param frame added frame, 0 if canceled
2354  */
2355 void Kid3Application::onTag2FrameAdded(const Frame* frame)
2356 {
2357   onFrameAdded(frame, Frame::Tag_2);
2358 }
2359 
2360 /**
2361  * Select a frame type and add such a frame to the frame list.
2362  * @param tagNr tag number
2363  */
2364 void Kid3Application::selectAndAddFrame(Frame::TagNumber tagNr)
2365 {
2366   addFrame(tagNr, nullptr, true);
2367 }
2368 
2369 /**
2370  * Edit a picture frame if one exists or add a new one.
2371  */
2372 void Kid3Application::editOrAddPicture()
2373 {
2374   if (m_framelist[Frame::Tag_Picture]->selectByName(QLatin1String("Picture"))) {
2375     editFrame(Frame::Tag_Picture);
2376   } else {
2377     PictureFrame frame;
2378     PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig());
2379     addFrame(Frame::Tag_Picture, &frame, true);
2380   }
2381 }
2382 
2383 /**
2384  * Open directory or add pictures on drop.
2385  *
2386  * @param paths paths of directories or files in directory
2387  * @param isInternal true if this is an internal drop
2388  */
2389 void Kid3Application::dropLocalFiles(const QStringList& paths, bool isInternal)
2390 {
2391   QStringList filePaths;
2392   QStringList picturePaths;
2393   for (QString txt : paths) {
2394     if (int lfPos = txt.indexOf(QLatin1Char('\n'));
2395         lfPos > 0 && lfPos < txt.length() - 1) {
2396       txt.truncate(lfPos + 1);
2397     }
2398     if (QString dir = txt.trimmed(); !dir.isEmpty()) {
2399       if (dir.endsWith(QLatin1String(".jpg"), Qt::CaseInsensitive) ||
2400           dir.endsWith(QLatin1String(".jpeg"), Qt::CaseInsensitive) ||
2401           dir.endsWith(QLatin1String(".webp"), Qt::CaseInsensitive) ||
2402           dir.endsWith(QLatin1String(".png"), Qt::CaseInsensitive)) {
2403         picturePaths.append(dir); // clazy:exclude=reserve-candidates
2404       } else {
2405         filePaths.append(dir); // clazy:exclude=reserve-candidates
2406       }
2407     }
2408   }
2409   if (!filePaths.isEmpty() && !isInternal) {
2410     resetFileFilterIfNotMatching(filePaths);
2411     emit fileSelectionUpdateRequested();
2412     emit confirmedOpenDirectoryRequested(filePaths);
2413   } else if (!picturePaths.isEmpty()) {
2414     const auto constPicturePaths = picturePaths;
2415     for (const QString& picturePath : constPicturePaths) {
2416       PictureFrame frame;
2417       if (PictureFrame::setDataFromFile(frame, picturePath)) {
2418         QString fileName(picturePath);
2419         if (int slashPos = fileName.lastIndexOf(QLatin1Char('/')); slashPos != -1) {
2420           fileName = fileName.mid(slashPos + 1);
2421         }
2422         PictureFrame::setMimeTypeFromFileName(frame, fileName);
2423         PictureFrame::setDescription(frame, fileName);
2424         PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig());
2425         addFrame(Frame::Tag_Picture, &frame);
2426         emit selectedFilesUpdated();
2427       }
2428     }
2429   }
2430 }
2431 
2432 /**
2433  * Open directory or add pictures on drop.
2434  *
2435  * @param paths paths of directories or files in directory
2436  */
2437 void Kid3Application::openDrop(const QStringList& paths)
2438 {
2439   dropLocalFiles(paths, false);
2440 }
2441 
2442 /**
2443  * Handle drop of URLs.
2444  *
2445  * @param urlList picture, tagged file and folder URLs to handle (if local)
2446  * @param isInternal true if this is an internal drop
2447  */
2448 void Kid3Application::dropUrls(const QList<QUrl>& urlList, bool isInternal)
2449 {
2450   QList urls(urlList);
2451 #ifdef Q_OS_MAC
2452   // workaround for https://bugreports.qt-project.org/browse/QTBUG-40449
2453   for (auto it = urls.begin(); it != urls.end(); ++it) {
2454     if (it->host().isEmpty() &&
2455         it->path().startsWith(QLatin1String("/.file/id="))) {
2456       *it = QUrl::fromCFURL(CFURLCreateFilePathURL(NULL, it->toCFURL(), NULL));
2457     }
2458   }
2459 #endif
2460   if (urls.isEmpty())
2461     return;
2462   if (urls.first().isLocalFile()) {
2463     QStringList localFiles;
2464     for (auto it = urls.constBegin(); it != urls.constEnd(); ++it) {
2465       localFiles.append(it->toLocalFile());
2466     }
2467     dropLocalFiles(localFiles, isInternal);
2468   } else {
2469     dropUrl(urls.first());
2470   }
2471 }
2472 
2473 /**
2474  * Handle drop of URLs.
2475  *
2476  * @param urlList picture, tagged file and folder URLs to handle (if local)
2477  */
2478 void Kid3Application::openDropUrls(const QList<QUrl>& urlList)
2479 {
2480   dropUrls(urlList, false);
2481 }
2482 
2483 /**
2484  * Add picture on drop.
2485  *
2486  * @param frame dropped picture frame
2487  */
2488 void Kid3Application::dropImage(Frame* frame)
2489 {
2490   PictureFrame::setTextEncoding(*frame, frameTextEncodingFromConfig());
2491   addFrame(Frame::Tag_Picture, frame);
2492   emit selectedFilesUpdated();
2493 }
2494 
2495 /**
2496  * Handle URL on drop.
2497  *
2498  * @param url dropped URL.
2499  */
2500 void Kid3Application::dropUrl(const QUrl& url)
2501 {
2502   downloadImage(url, Kid3Application::ImageForSelectedFiles);
2503 }
2504 
2505 /**
2506  * Add a downloaded image.
2507  *
2508  * @param data     HTTP response of download
2509  * @param mimeType MIME type of data
2510  * @param url      URL of downloaded data
2511  */
2512 void Kid3Application::imageDownloaded(const QByteArray& data,
2513                               const QString& mimeType, const QString& url)
2514 {
2515   // An empty mime type is accepted to allow downloads via FTP.
2516   if (mimeType.startsWith(QLatin1String("image")) ||
2517       mimeType.isEmpty()) {
2518     PictureFrame frame(data, url, PictureFrame::PT_CoverFront, mimeType,
2519                        frameTextEncodingFromConfig());
2520     if (getDownloadImageDestination() == ImageForAllFilesInDirectory) {
2521       TaggedFileOfDirectoryIterator it(currentOrRootIndex());
2522       while (it.hasNext()) {
2523         TaggedFile* taggedFile = it.next();
2524         taggedFile->readTags(false);
2525         taggedFile->addFrame(Frame::Tag_Picture, frame);
2526       }
2527     } else if (getDownloadImageDestination() == ImageForImportTrackData) {
2528       const ImportTrackDataVector& trackDataVector(
2529             getTrackDataModel()->trackData());
2530       for (auto it = trackDataVector.constBegin();
2531            it != trackDataVector.constEnd();
2532            ++it) {
2533         if (TaggedFile* taggedFile;
2534             it->isEnabled() && (taggedFile = it->getTaggedFile()) != nullptr) {
2535           taggedFile->readTags(false);
2536           taggedFile->addFrame(Frame::Tag_Picture, frame);
2537         }
2538       }
2539     } else {
2540       addFrame(Frame::Tag_Picture, &frame);
2541     }
2542     emit selectedFilesUpdated();
2543   }
2544 }
2545 
2546 /**
2547  * Set the first file as the current file.
2548  *
2549  * @param select true to select the file
2550  * @param onlyTaggedFiles only consider tagged files
2551  *
2552  * @return true if a file exists.
2553  */
2554 bool Kid3Application::firstFile(bool select, bool onlyTaggedFiles)
2555 {
2556   m_fileSelectionModel->setCurrentIndex(getRootIndex(),
2557                                         QItemSelectionModel::NoUpdate);
2558   return nextFile(select, onlyTaggedFiles);
2559 }
2560 
2561 /**
2562  * Set the next file as the current file.
2563  *
2564  * @param select true to select the file
2565  * @param onlyTaggedFiles only consider tagged files
2566  *
2567  * @return true if a next file exists.
2568  */
2569 bool Kid3Application::nextFile(bool select, bool onlyTaggedFiles)
2570 {
2571   QModelIndex next(m_fileSelectionModel->currentIndex());
2572   do {
2573     QModelIndex current = next;
2574     next = QModelIndex();
2575     if (m_fileProxyModel->rowCount(current) > 0) {
2576       // to first child
2577       next = m_fileProxyModel->index(0, 0, current);
2578     } else {
2579       QModelIndex parent = current;
2580       while (!next.isValid() && parent.isValid()) {
2581         // to next sibling or next sibling of parent
2582         int row = parent.row();
2583         if (parent == getRootIndex() || !parent.isValid()) {
2584           // do not move beyond root index
2585           return false;
2586         }
2587         parent = parent.parent();
2588         if (row + 1 < m_fileProxyModel->rowCount(parent)) {
2589           // to next sibling
2590           next = m_fileProxyModel->index(row + 1, 0, parent);
2591         }
2592       }
2593     }
2594   } while (onlyTaggedFiles && !FileProxyModel::getTaggedFileOfIndex(next));
2595   if (!next.isValid())
2596     return false;
2597   m_fileSelectionModel->setCurrentIndex(next,
2598     select ? QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows
2599            : QItemSelectionModel::Current);
2600   return true;
2601 }
2602 
2603 /**
2604  * Set the previous file as the current file.
2605  *
2606  * @param select true to select the file
2607  * @param onlyTaggedFiles only consider tagged files
2608  *
2609  * @return true if a previous file exists.
2610  */
2611 bool Kid3Application::previousFile(bool select, bool onlyTaggedFiles)
2612 {
2613   QModelIndex previous(m_fileSelectionModel->currentIndex());
2614   do {
2615     QModelIndex current = previous;
2616     previous = QModelIndex();
2617     if (int row = current.row() - 1; row >= 0) {
2618       // to last leafnode of previous sibling
2619       previous = current.sibling(row, 0);
2620       row = m_fileProxyModel->rowCount(previous) - 1;
2621       while (row >= 0) {
2622         previous = m_fileProxyModel->index(row, 0, previous);
2623         row = m_fileProxyModel->rowCount(previous) - 1;
2624       }
2625     } else {
2626       // to parent
2627       previous = current.parent();
2628     }
2629     if (previous == getRootIndex() || !previous.isValid()) {
2630       return false;
2631     }
2632   } while (onlyTaggedFiles && !FileProxyModel::getTaggedFileOfIndex(previous));
2633   if (!previous.isValid())
2634     return false;
2635   m_fileSelectionModel->setCurrentIndex(previous,
2636     select ? QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows
2637            : QItemSelectionModel::Current);
2638   return true;
2639 }
2640 
2641 /**
2642  * Select or deselect the current file.
2643  *
2644  * @param select true to select the file, false to deselect it
2645  *
2646  * @return true if a current file exists.
2647  */
2648 bool Kid3Application::selectCurrentFile(bool select)
2649 {
2650   QModelIndex currentIdx(m_fileSelectionModel->currentIndex());
2651   if (!currentIdx.isValid() || currentIdx == getRootIndex())
2652     return false;
2653 
2654   m_fileSelectionModel->setCurrentIndex(currentIdx,
2655     (select ? QItemSelectionModel::Select : QItemSelectionModel::Deselect) |
2656     QItemSelectionModel::Rows);
2657   return true;
2658 }
2659 
2660 /**
2661  * Select all files.
2662  */
2663 void Kid3Application::selectAllFiles()
2664 {
2665   QItemSelection selection;
2666   ModelIterator it(m_fileProxyModelRootIndex);
2667   while (it.hasNext()) {
2668     selection.append(QItemSelectionRange(it.next()));
2669   }
2670   m_fileSelectionModel->select(selection,
2671       QItemSelectionModel::Select | QItemSelectionModel::Rows);
2672 }
2673 
2674 /**
2675  * Deselect all files.
2676  */
2677 void Kid3Application::deselectAllFiles()
2678 {
2679   m_fileSelectionModel->clearSelection();
2680 }
2681 
2682 /**
2683  * Select all files in the current directory.
2684  */
2685 void Kid3Application::selectAllInDirectory()
2686 {
2687   if (QModelIndex parent = m_fileSelectionModel->currentIndex();
2688       parent.isValid()) {
2689     if (!m_fileProxyModel->hasChildren(parent)) {
2690       parent = parent.parent();
2691     }
2692     QItemSelection selection;
2693     for (int row = 0; row < m_fileProxyModel->rowCount(parent); ++row) {
2694       if (QModelIndex index = m_fileProxyModel->index(row, 0, parent);
2695           !m_fileProxyModel->hasChildren(index)) {
2696         selection.append(QItemSelectionRange(index)); // clazy:exclude=reserve-candidates
2697       }
2698     }
2699     m_fileSelectionModel->select(selection,
2700                      QItemSelectionModel::Select | QItemSelectionModel::Rows);
2701   }
2702 }
2703 
2704 /**
2705  * Invert current selection.
2706  */
2707 void Kid3Application::invertSelection()
2708 {
2709   QModelIndexList todo;
2710   todo.append(m_fileProxyModelRootIndex);
2711   while (!todo.isEmpty()) {
2712     QModelIndex parent = todo.takeFirst();
2713     QModelIndex first, last;
2714     for (int row = 0, numRows = m_fileProxyModel->rowCount(parent);
2715          row < numRows;
2716          ++row) {
2717       QModelIndex idx = m_fileProxyModel->index(row, 0, parent);
2718       if (row == 0) {
2719         first = idx;
2720       } else if (row == numRows - 1) {
2721         last = idx;
2722       }
2723       if (m_fileProxyModel->hasChildren(idx)) {
2724         todo.append(idx);
2725       }
2726     }
2727     m_fileSelectionModel->select(
2728           QItemSelection(first, last),
2729           QItemSelectionModel::Toggle | QItemSelectionModel::Rows);
2730   }
2731 }
2732 
2733 /**
2734  * Set a specific file as the current file.
2735  *
2736  * @param filePath path to file
2737  * @param select true to select the file
2738  *
2739  * @return true if file exists.
2740  */
2741 bool Kid3Application::selectFile(const QString& filePath, bool select)
2742 {
2743   QModelIndex index = m_fileProxyModel->index(filePath);
2744   if (!index.isValid())
2745     return false;
2746 
2747   m_fileSelectionModel->setCurrentIndex(index,
2748     select ? QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows
2749            : QItemSelectionModel::Current);
2750   return true;
2751 }
2752 
2753 /**
2754  * Get paths to all selected files.
2755  * @param onlyTaggedFiles only consider tagged files
2756  * @return list of absolute file paths.
2757  */
2758 QStringList Kid3Application::getSelectedFilePaths(bool onlyTaggedFiles) const
2759 {
2760   QStringList files;
2761   const QModelIndexList selItems = m_fileSelectionModel->selectedRows();
2762   if (onlyTaggedFiles) {
2763     for (const QModelIndex& index : selItems) {
2764       if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index))
2765       {
2766         files.append(taggedFile->getAbsFilename());
2767       }
2768     }
2769   } else {
2770     files.reserve(selItems.size());
2771     for (const QModelIndex& index : selItems) {
2772       files.append(m_fileProxyModel->filePath(index));
2773     }
2774   }
2775   return files;
2776 }
2777 
2778 /**
2779  * Fetch entries of directory if not already fetched.
2780  * This works like FileList::expand(), but without expanding tree view
2781  * items and independent of the GUI. The processing is done in the background
2782  * by FileSystemModel, so the fetched items are not immediately available
2783  * after calling this method.
2784  *
2785  * @param index index of directory item
2786  */
2787 void Kid3Application::fetchDirectory(const QModelIndex& index)
2788 {
2789   if (index.isValid() && m_fileProxyModel->canFetchMore(index)) {
2790     m_fileProxyModel->fetchMore(index);
2791   }
2792 }
2793 
2794 /**
2795  * Fetch entries of directory and toggle expanded state if GUI available.
2796  * @param index index of directory item
2797  */
2798 void Kid3Application::expandDirectory(const QModelIndex& index)
2799 {
2800   fetchDirectory(index);
2801   emit toggleExpandedRequested(index);
2802 }
2803 
2804 /**
2805  * Expand the whole file list if GUI available.
2806  * expandFileListFinished() is emitted when finished.
2807  */
2808 void Kid3Application::requestExpandFileList()
2809 {
2810   emit expandFileListRequested();
2811 }
2812 
2813 /**
2814  * Called when operation for requestExpandFileList() is finished.
2815  */
2816 void Kid3Application::notifyExpandFileListFinished()
2817 {
2818   emit expandFileListFinished();
2819 }
2820 
2821 /**
2822  * Process change of selection.
2823  * The GUI is signaled to update the current selection and the controls.
2824  * @param selected selected items
2825  * @param deselected deselected items
2826  */
2827 void Kid3Application::fileSelected(const QItemSelection& selected,
2828                                    const QItemSelection& deselected)
2829 {
2830   emit fileSelectionUpdateRequested();
2831   emit selectedFilesChanged(selected, deselected);
2832 }
2833 
2834 /**
2835  * Search in tags for a given text.
2836  * @param params search parameters
2837  */
2838 void Kid3Application::findText(const TagSearcher::Parameters& params)
2839 {
2840   m_tagSearcher->setModel(m_fileProxyModel);
2841   m_tagSearcher->setRootIndex(m_fileProxyModelRootIndex);
2842   m_tagSearcher->find(params);
2843 }
2844 
2845 /**
2846  * Replace found text.
2847  * @param params search parameters
2848  */
2849 void Kid3Application::replaceText(const TagSearcher::Parameters& params)
2850 {
2851   m_tagSearcher->setModel(m_fileProxyModel);
2852   m_tagSearcher->setRootIndex(m_fileProxyModelRootIndex);
2853   m_tagSearcher->replace(params);
2854 }
2855 
2856 /**
2857  * Replace all occurrences.
2858  * @param params search parameters
2859  */
2860 void Kid3Application::replaceAll(const TagSearcher::Parameters& params)
2861 {
2862   m_tagSearcher->setModel(m_fileProxyModel);
2863   m_tagSearcher->setRootIndex(m_fileProxyModelRootIndex);
2864   m_tagSearcher->replaceAll(params);
2865 }
2866 
2867 /**
2868  * Schedule actions to rename a directory.
2869  * When finished renameActionsScheduled() is emitted.
2870  */
2871 void Kid3Application::scheduleRenameActions()
2872 {
2873   m_dirRenamer->clearActions();
2874   m_dirRenamer->clearAborted();
2875   // If directories are selected, rename them, else process files of the
2876   // current directory.
2877   QList<QPersistentModelIndex> indexes;
2878   const auto selectedIndexes = m_fileSelectionModel->selectedRows();
2879   for (const QModelIndex& index : selectedIndexes) {
2880     if (m_fileProxyModel->isDir(index)) {
2881       indexes.append(index);
2882     }
2883   }
2884   if (indexes.isEmpty()) {
2885     indexes.append(m_fileProxyModelRootIndex);
2886   }
2887 
2888   connect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady,
2889           this, &Kid3Application::scheduleNextRenameAction);
2890   m_fileProxyModelIterator->start(indexes);
2891 }
2892 
2893 /**
2894  * Schedule rename action for a file.
2895  *
2896  * @param index index of file in file proxy model
2897  */
2898 void Kid3Application::scheduleNextRenameAction(const QPersistentModelIndex& index)
2899 {
2900   bool terminated = !index.isValid();
2901   if (!terminated) {
2902     if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
2903       taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
2904       m_dirRenamer->scheduleAction(taggedFile);
2905       if (m_dirRenamer->isAborted()) {
2906         terminated = true;
2907       }
2908     }
2909   }
2910   if (terminated) {
2911     m_fileProxyModelIterator->abort();
2912     disconnect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady,
2913                this, &Kid3Application::scheduleNextRenameAction);
2914     m_dirRenamer->endScheduleActions();
2915     emit renameActionsScheduled();
2916   }
2917 }
2918 
2919 /**
2920  * Open directory after resetting the file system model.
2921  * When finished directoryOpened() is emitted, also if false is returned.
2922  *
2923  * @param paths file or directory paths, if multiple paths are given, the
2924  * common directory is opened and the files are selected, if empty, the
2925  * currently open directory is reopened
2926  *
2927  * @return true if ok.
2928  */
2929 bool Kid3Application::openDirectoryAfterReset(const QStringList& paths)
2930 {
2931   // Clear the selection.
2932   m_selection->beginAddTaggedFiles();
2933   m_selection->endAddTaggedFiles();
2934   QStringList dirs(paths);
2935   if (dirs.isEmpty()) {
2936     dirs.append(m_fileSystemModel->rootPath());
2937   }
2938   m_fileSystemModel->clear();
2939   return openDirectory(dirs);
2940 }
2941 
2942 /**
2943  * Apply file filter after the file system model has been reset.
2944  */
2945 void Kid3Application::applyFilterAfterReset()
2946 {
2947   disconnect(this, &Kid3Application::directoryOpened,
2948              this, &Kid3Application::applyFilterAfterReset);
2949   proceedApplyingFilter();
2950 }
2951 
2952 /**
2953  * Apply a file filter.
2954  *
2955  * @param fileFilter filter to apply.
2956  */
2957 void Kid3Application::applyFilter(FileFilter& fileFilter)
2958 {
2959   m_fileFilter = &fileFilter;
2960   /*
2961    * When a lot of files are filtered out,
2962    * QSortFilterProxyModel::invalidateFilter() is extremely slow (probably
2963    * depending on the source model). In this case, I measured
2964    * 3s for 3000 files, 8s for 5000 files, 54s for 10000 files, and too long
2965    * to wait for more files. If such a case is detected, the file system model
2966    * is recreated in order to avoid calling invalidateFilter().
2967    */
2968   if (m_filterTotal - m_filterPassed > 4000) {
2969     connect(this, &Kid3Application::directoryOpened,
2970             this, &Kid3Application::applyFilterAfterReset);
2971     openDirectoryAfterReset();
2972   } else {
2973     m_fileProxyModel->disableFilteringOutIndexes();
2974     proceedApplyingFilter();
2975   }
2976 }
2977 
2978 /**
2979  * Second stage for applyFilter().
2980  */
2981 void Kid3Application::proceedApplyingFilter()
2982 {
2983   const bool justClearingFilter =
2984       m_fileFilter->isEmptyFilterExpression() && isFiltered();
2985   setFiltered(false);
2986   m_fileFilter->clearAborted();
2987   m_filterPassed = 0;
2988   m_filterTotal = 0;
2989   emit fileFiltered(FileFilter::Started, QString(),
2990                     m_filterPassed, m_filterTotal);
2991 
2992   m_lastProcessedDirName.clear();
2993   if (!justClearingFilter) {
2994     connect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady,
2995             this, &Kid3Application::filterNextFile);
2996     m_fileProxyModelIterator->start(m_fileProxyModelRootIndex);
2997   } else {
2998     emit fileFiltered(FileFilter::Finished, QString(),
2999                       m_filterPassed, m_filterTotal);
3000   }
3001 }
3002 
3003 /**
3004  * Apply single file to file filter.
3005  *
3006  * @param index index of file in file proxy model
3007  */
3008 void Kid3Application::filterNextFile(const QPersistentModelIndex& index)
3009 {
3010   if (!m_fileFilter)
3011     return;
3012 
3013   bool terminated = !index.isValid();
3014   if (!terminated) {
3015     if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
3016       bool tagInfoRead = taggedFile->isTagInformationRead();
3017       taggedFile = FileProxyModel::readTagsFromTaggedFile(taggedFile);
3018       if (taggedFile->getDirname() != m_lastProcessedDirName) {
3019         m_lastProcessedDirName = taggedFile->getDirname();
3020         emit fileFiltered(FileFilter::Directory, m_lastProcessedDirName,
3021                           m_filterPassed, m_filterTotal);
3022       }
3023       bool ok;
3024       bool pass = m_fileFilter->filter(*taggedFile, &ok);
3025       if (ok) {
3026         ++m_filterTotal;
3027         if (pass) {
3028           ++m_filterPassed;
3029         }
3030         emit fileFiltered(
3031               pass ? FileFilter::FilePassed : FileFilter::FileFilteredOut,
3032               taggedFile->getFilename(), m_filterPassed, m_filterTotal);
3033         if (!pass)
3034           m_fileProxyModel->filterOutIndex(taggedFile->getIndex());
3035       } else {
3036         emit fileFiltered(FileFilter::ParseError, QString(),
3037                           m_filterPassed, m_filterTotal);
3038         terminated = true;
3039       }
3040 
3041       // Free resources if tag was not read before filtering
3042       if (!pass && !tagInfoRead) {
3043         taggedFile->clearTags(false);
3044       }
3045 
3046       if (m_fileFilter->isAborted()) {
3047         terminated = true;
3048         emit fileFiltered(FileFilter::Aborted, QString(),
3049                           m_filterPassed, m_filterTotal);
3050       }
3051     }
3052   }
3053   if (terminated) {
3054     if (!m_fileFilter->isAborted()) {
3055       emit fileFiltered(FileFilter::Finished, QString(),
3056                         m_filterPassed, m_filterTotal);
3057     }
3058 
3059     m_fileProxyModelIterator->abort();
3060     m_fileProxyModel->applyFilteringOutIndexes();
3061     setFiltered(!m_fileFilter->isEmptyFilterExpression());
3062 
3063     disconnect(m_fileProxyModelIterator, &FileProxyModelIterator::nextReady,
3064                this, &Kid3Application::filterNextFile);
3065   }
3066 }
3067 
3068 /**
3069  * Apply a file filter.
3070  *
3071  * @param expression filter expression
3072  */
3073 void Kid3Application::applyFilter(const QString& expression)
3074 {
3075   if (!m_expressionFileFilter) {
3076     m_expressionFileFilter = new FileFilter(this);
3077   }
3078   m_expressionFileFilter->clearAborted();
3079   m_expressionFileFilter->setFilterExpression(expression);
3080   m_expressionFileFilter->initParser();
3081   applyFilter(*m_expressionFileFilter);
3082 }
3083 
3084 /**
3085  * Abort expression file filter.
3086  */
3087 void Kid3Application::abortFilter()
3088 {
3089   if (m_expressionFileFilter) {
3090     m_expressionFileFilter->abort();
3091   }
3092 }
3093 
3094 /**
3095  * Perform rename actions and change application directory afterwards if it
3096  * was renamed.
3097  *
3098  * @return error messages, null string if no error occurred.
3099  */
3100 QString Kid3Application::performRenameActions()
3101 {
3102   QString errorMsg;
3103   m_dirRenamer->setDirName(getDirName());
3104   m_dirRenamer->performActions(&errorMsg);
3105   if (m_dirRenamer->getDirName() != getDirName()) {
3106     openDirectory({m_dirRenamer->getDirName()});
3107   }
3108   return errorMsg;
3109 }
3110 
3111 /**
3112  * Reset the file system model and then try to perform the rename actions.
3113  * On Windows, renaming directories fails when they have a subdirectory which
3114  * is open in the file system model. This method can be used to retry in such
3115  * a situation.
3116  */
3117 void Kid3Application::tryRenameActionsAfterReset()
3118 {
3119   connect(this, &Kid3Application::directoryOpened,
3120           this, &Kid3Application::performRenameActionsAfterReset);
3121   openDirectoryAfterReset();
3122 }
3123 
3124 /**
3125  * Perform rename actions after the file system model has been reset.
3126  */
3127 void Kid3Application::performRenameActionsAfterReset()
3128 {
3129   disconnect(this, &Kid3Application::directoryOpened,
3130              this, &Kid3Application::performRenameActionsAfterReset);
3131   performRenameActions();
3132 }
3133 
3134 /**
3135  * Reset the file system model and then try to rename a file.
3136  * On Windows, renaming directories fails when they have a subdirectory which
3137  * is open in the file system model. This method can be used to retry in such
3138  * a situation.
3139  *
3140  * @param oldName old file name
3141  * @param newName new file name
3142  */
3143 void Kid3Application::tryRenameAfterReset(const QString& oldName,
3144                                           const QString& newName)
3145 {
3146   m_renameAfterResetOldName = oldName;
3147   m_renameAfterResetNewName = newName;
3148   connect(this, &Kid3Application::directoryOpened,
3149           this, &Kid3Application::renameAfterReset);
3150   openDirectoryAfterReset();
3151 }
3152 
3153 /**
3154  * Rename after the file system model has been reset.
3155  */
3156 void Kid3Application::renameAfterReset()
3157 {
3158   disconnect(this, &Kid3Application::directoryOpened, this, &Kid3Application::renameAfterReset);
3159   if (!m_renameAfterResetOldName.isEmpty() &&
3160       !m_renameAfterResetNewName.isEmpty()) {
3161     Utils::safeRename(m_renameAfterResetOldName, m_renameAfterResetNewName);
3162     m_renameAfterResetOldName.clear();
3163     m_renameAfterResetNewName.clear();
3164   }
3165 }
3166 
3167 /**
3168  * Set the directory name from the tags.
3169  * The directory must not have modified files.
3170  * renameActionsScheduled() is emitted when the rename actions have been
3171  * scheduled. Then performRenameActions() has to be called to effectively
3172  * rename the directory.
3173  *
3174  * @param tagMask tag mask
3175  * @param format  directory name format
3176  * @param create  true to create, false to rename
3177  *
3178  * @return true if ok.
3179  */
3180 bool Kid3Application::renameDirectory(Frame::TagVersion tagMask,
3181                                      const QString& format, bool create)
3182 {
3183   if (TaggedFile* taggedFile =
3184         TaggedFileOfDirectoryIterator::first(currentOrRootIndex());
3185       !isModified() && taggedFile) {
3186     m_dirRenamer->setTagVersion(tagMask);
3187     m_dirRenamer->setFormat(format);
3188     m_dirRenamer->setAction(create);
3189     scheduleRenameActions();
3190     return true;
3191   }
3192   return false;
3193 }
3194 
3195 /**
3196  * Check modification state.
3197  *
3198  * @return true if a file is modified.
3199  */
3200 bool Kid3Application::isModified() const
3201 {
3202   return m_fileProxyModel->isModified();
3203 }
3204 
3205 /**
3206  * Number tracks in selected files of directory.
3207  *
3208  * @param nr start number
3209  * @param total total number of tracks, used if >0
3210  * @param tagVersion determines on which tags the numbers are set
3211  * @param options options for numbering operation
3212  */
3213 void Kid3Application::numberTracks(int nr, int total,
3214                                    Frame::TagVersion tagVersion,
3215                                    NumberTrackOptions options)
3216 {
3217   QString lastDirName;
3218   bool totalEnabled = TagConfig::instance().enableTotalNumberOfTracks();
3219   bool directoryMode = true;
3220   int startNr = nr;
3221   emit fileSelectionUpdateRequested();
3222   int numDigits = TagConfig::instance().trackNumberDigits();
3223   if (numDigits < 1 || numDigits > 5)
3224     numDigits = 1;
3225 
3226   // If directories are selected, number their files, else process the selected
3227   // files of the current directory.
3228   AbstractTaggedFileIterator* it =
3229       new TaggedFileOfSelectedDirectoriesIterator(getFileSelectionModel());
3230   if (!it->hasNext()) {
3231     delete it;
3232     it = new SelectedTaggedFileOfDirectoryIterator(
3233                currentOrRootIndex(),
3234                getFileSelectionModel(),
3235                true);
3236     directoryMode = false;
3237   }
3238   while (it->hasNext()) {
3239     TaggedFile* taggedFile = it->next();
3240     taggedFile->readTags(false);
3241     if (options & NumberTracksResetCounterForEachDirectory) {
3242       if (QString dirName = taggedFile->getDirname(); lastDirName != dirName) {
3243         nr = startNr;
3244         if (totalEnabled && directoryMode) {
3245           total = taggedFile->getTotalNumberOfTracksInDir();
3246         }
3247         lastDirName = dirName;
3248       }
3249     }
3250     FOR_TAGS_IN_MASK(tagNr, tagVersion) {
3251       if (tagNr == Frame::Tag_Id3v1) {
3252         if (options & NumberTracksEnabled) {
3253           QString value;
3254           value.setNum(nr);
3255           if (Frame frame;
3256               taggedFile->getFrame(tagNr, Frame::FT_Track, frame)) {
3257             frame.setValueIfChanged(value);
3258             if (frame.isValueChanged()) {
3259               taggedFile->setFrame(tagNr, frame);
3260             }
3261           } else {
3262             frame.setValue(value);
3263             frame.setExtendedType(Frame::ExtendedType(Frame::FT_Track));
3264             taggedFile->setFrame(tagNr, frame);
3265           }
3266         }
3267       } else {
3268         // For tag 2 the frame is written, so that we have control over the
3269         // format and the total number of tracks, and it is possible to change
3270         // the format even if the numbers stay the same.
3271         FrameCollection frames;
3272         taggedFile->getAllFrames(tagNr, frames);
3273         Frame frame(Frame::FT_Track, QLatin1String(""), QLatin1String(""), -1);
3274         auto frameIt = frames.find(frame);
3275         QString value;
3276         if (options & NumberTracksEnabled) {
3277           if (total > 0) {
3278             value = QString(QLatin1String("%1/%2"))
3279                 .arg(nr, numDigits, 10, QLatin1Char('0'))
3280                 .arg(total, numDigits, 10, QLatin1Char('0'));
3281           } else {
3282             value = QString(QLatin1String("%1"))
3283                 .arg(nr, numDigits, 10, QLatin1Char('0'));
3284           }
3285           if (frameIt != frames.end()) {
3286             frame = *frameIt;
3287             frame.setValueIfChanged(value);
3288             if (frame.isValueChanged()) {
3289               taggedFile->setFrame(tagNr, frame);
3290             }
3291           } else {
3292             frame.setValue(value);
3293             frame.setExtendedType(Frame::ExtendedType(Frame::FT_Track));
3294             taggedFile->setFrame(tagNr, frame);
3295           }
3296         } else {
3297           // If track numbering is not enabled, just reformat the current value.
3298           if (frameIt != frames.end()) {
3299             frame = *frameIt;
3300             int currentTotal;
3301             int currentNr = TaggedFile::splitNumberAndTotal(frame.getValue(),
3302                                                             &currentTotal);
3303             // Set the total if enabled.
3304             if (totalEnabled && total > 0) {
3305               currentTotal = total;
3306             }
3307             if (currentTotal > 0) {
3308               value = QString(QLatin1String("%1/%2"))
3309                   .arg(currentNr, numDigits, 10, QLatin1Char('0'))
3310                   .arg(currentTotal, numDigits, 10, QLatin1Char('0'));
3311             } else {
3312               value = QString(QLatin1String("%1"))
3313                   .arg(currentNr, numDigits, 10, QLatin1Char('0'));
3314             }
3315             frame.setValueIfChanged(value);
3316             if (frame.isValueChanged()) {
3317               taggedFile->setFrame(tagNr, frame);
3318             }
3319           }
3320         }
3321       }
3322     }
3323     ++nr;
3324   }
3325   emit selectedFilesUpdated();
3326   delete it;
3327 }
3328 
3329 /**
3330  * Play audio file.
3331  */
3332 void Kid3Application::playAudio()
3333 {
3334   QObject* player = getAudioPlayer();
3335   if (!player)
3336     return;
3337 
3338   QStringList files;
3339   int fileNr = 0;
3340   if (QModelIndexList selectedRows = m_fileSelectionModel->selectedRows();
3341       selectedRows.size() > 1) {
3342     // play only the selected files if more than one is selected
3343     SelectedTaggedFileIterator it(m_fileProxyModelRootIndex,
3344                                   m_fileSelectionModel,
3345                                   false);
3346     while (it.hasNext()) {
3347       files.append(it.next()->getAbsFilename());
3348     }
3349   } else {
3350     if (selectedRows.size() == 1) {
3351       // If a playlist file is selected, play the files in the playlist.
3352       QModelIndex index = selectedRows.first();
3353       index = index.sibling(index.row(), 0);
3354       QString path = m_fileProxyModel->filePath(index);
3355       bool isPlaylist = false;
3356       PlaylistConfig::formatFromFileExtension(path, &isPlaylist);
3357       if (isPlaylist) {
3358         files = playlistModel(path)->pathsInPlaylist();
3359       }
3360     }
3361     if (files.isEmpty()) {
3362       // play all files if none or only one is selected
3363       int idx = 0;
3364       ModelIterator it(m_fileProxyModelRootIndex);
3365       while (it.hasNext()) {
3366         QModelIndex index = it.next();
3367         if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
3368           files.append(taggedFile->getAbsFilename());
3369           if (m_fileSelectionModel->isSelected(index)) {
3370             fileNr = idx;
3371           }
3372           ++idx;
3373         }
3374       }
3375     }
3376   }
3377   emit aboutToPlayAudio();
3378   QMetaObject::invokeMethod(player, "setFiles",
3379                             Q_ARG(QStringList, files), Q_ARG(int, fileNr));
3380 }
3381 
3382 /**
3383  * Show play tool bar.
3384  */
3385 void Kid3Application::showAudioPlayer()
3386 {
3387   emit aboutToPlayAudio();
3388 }
3389 
3390 /**
3391  * Get number of tracks in current directory.
3392  *
3393  * @return number of tracks, 0 if not found.
3394  */
3395 int Kid3Application::getTotalNumberOfTracksInDir() const
3396 {
3397   if (TaggedFile* taggedFile = TaggedFileOfDirectoryIterator::first(
3398       currentOrRootIndex())) {
3399     return taggedFile->getTotalNumberOfTracksInDir();
3400   }
3401   return 0;
3402 }
3403 
3404 /**
3405  * Create a filter string for the file dialog.
3406  * The filter string contains entries for all supported types.
3407  *
3408  * @return filter string.
3409  */
3410 QString Kid3Application::createFilterString() const
3411 {
3412   return m_platformTools->fileDialogNameFilter(
3413         FileProxyModel::createNameFilters());
3414 }
3415 
3416 /**
3417  * Remove the file filter if necessary to open the files.
3418  * @param filePaths paths to files or directories
3419  */
3420 void Kid3Application::resetFileFilterIfNotMatching(const QStringList& filePaths)
3421 {
3422   if (QStringList nameFilters(m_platformTools->getNameFilterPatterns(
3423         FileConfig::instance().nameFilter()).split(QLatin1Char(' ')));
3424       !nameFilters.isEmpty() && nameFilters.first() != QLatin1String("*")) {
3425     for (const QString& filePath : filePaths) {
3426       if (QFileInfo fi(filePath);
3427           !QDir::match(nameFilters, fi.fileName()) && !fi.isDir()) {
3428         setAllFilesFileFilter();
3429         break;
3430       }
3431     }
3432   }
3433 }
3434 
3435 /**
3436  * Set file name filter for "All Files (*)".
3437  */
3438 void Kid3Application::setAllFilesFileFilter() {
3439   FileConfig::instance().setNameFilter(
3440         m_platformTools->fileDialogNameFilter(
3441           QList<QPair<QString, QString> >()
3442           << qMakePair(tr("All Files"), QString(QLatin1Char('*')))));
3443 }
3444 
3445 /**
3446  * Notify the tagged file factories about the changed configuration.
3447  */
3448 void Kid3Application::notifyConfigurationChange()
3449 {
3450   const auto factories = FileProxyModel::taggedFileFactories();
3451   for (ITaggedFileFactory* factory : factories) {
3452     const auto keys = factory->taggedFileKeys();
3453     for (const QString& key : keys) {
3454       factory->notifyConfigurationChange(key);
3455     }
3456   }
3457 }
3458 
3459 /**
3460  * Convert ID3v2.3 to ID3v2.4 tags.
3461  */
3462 void Kid3Application::convertToId3v24()
3463 {
3464   emit fileSelectionUpdateRequested();
3465   SelectedTaggedFileIterator it(getRootIndex(),
3466                                 getFileSelectionModel(),
3467                                 false);
3468   while (it.hasNext()) {
3469     TaggedFile* taggedFile = it.next();
3470     taggedFile->readTags(false);
3471     if (taggedFile->hasTag(Frame::Tag_Id3v2) && !taggedFile->isChanged()) {
3472       if (QString tagFmt = taggedFile->getTagFormat(Frame::Tag_Id3v2);
3473           tagFmt.length() >= 7 && tagFmt.startsWith(QLatin1String("ID3v2.")) &&
3474           tagFmt[6] < QLatin1Char('4')) {
3475         if ((taggedFile->taggedFileFeatures() &
3476              (TaggedFile::TF_ID3v23 | TaggedFile::TF_ID3v24)) ==
3477               TaggedFile::TF_ID3v23) {
3478           FrameCollection frames;
3479           taggedFile->getAllFrames(Frame::Tag_Id3v2, frames);
3480           FrameFilter flt;
3481           flt.enableAll();
3482           taggedFile->deleteFrames(Frame::Tag_Id3v2, flt);
3483 
3484           // The file has to be reread to write ID3v2.4 tags
3485           taggedFile = FileProxyModel::readWithId3V24(taggedFile);
3486 
3487           // Restore the frames
3488           FrameFilter frameFlt;
3489           frameFlt.enableAll();
3490           taggedFile->setFrames(Frame::Tag_Id3v2,
3491                                 frames.copyEnabledFrames(frameFlt), false);
3492         }
3493 
3494         // Write the file with ID3v2.4 tags
3495         bool renamed;
3496         int storedFeatures = taggedFile->activeTaggedFileFeatures();
3497         taggedFile->setActiveTaggedFileFeatures(TaggedFile::TF_ID3v24);
3498         taggedFile->writeTags(true, &renamed,
3499                               FileConfig::instance().preserveTime());
3500         taggedFile->setActiveTaggedFileFeatures(storedFeatures);
3501         taggedFile->readTags(true);
3502       }
3503     }
3504   }
3505   emit selectedFilesUpdated();
3506 }
3507 
3508 /**
3509  * Convert ID3v2.4 to ID3v2.3 tags.
3510  */
3511 void Kid3Application::convertToId3v23()
3512 {
3513   emit fileSelectionUpdateRequested();
3514   SelectedTaggedFileIterator it(getRootIndex(),
3515                                 getFileSelectionModel(),
3516                                 false);
3517   while (it.hasNext()) {
3518     TaggedFile* taggedFile = it.next();
3519     taggedFile->readTags(false);
3520     if (taggedFile->hasTag(Frame::Tag_Id3v2) && !taggedFile->isChanged()) {
3521       QString tagFmt = taggedFile->getTagFormat(Frame::Tag_Id3v2);
3522       if (QString ext = taggedFile->getFileExtension();
3523           tagFmt.length() >= 7 && tagFmt.startsWith(QLatin1String("ID3v2.")) &&
3524           tagFmt[6] > QLatin1Char('3') &&
3525           (ext == QLatin1String(".mp3") || ext == QLatin1String(".mp2") ||
3526            ext == QLatin1String(".aac") || ext == QLatin1String(".wav") ||
3527            ext == QLatin1String(".dsf") || ext == QLatin1String(".dff"))) {
3528         if (!(taggedFile->taggedFileFeatures() & TaggedFile::TF_ID3v23)) {
3529           FrameCollection frames;
3530           taggedFile->getAllFrames(Frame::Tag_Id3v2, frames);
3531           FrameFilter flt;
3532           flt.enableAll();
3533           taggedFile->deleteFrames(Frame::Tag_Id3v2, flt);
3534 
3535           // The file has to be reread to write ID3v2.3 tags
3536           taggedFile = FileProxyModel::readWithId3V23(taggedFile);
3537 
3538           // Restore the frames
3539           FrameFilter frameFlt;
3540           frameFlt.enableAll();
3541           taggedFile->setFrames(Frame::Tag_Id3v2,
3542                                 frames.copyEnabledFrames(frameFlt), false);
3543         }
3544 
3545         // Write the file with ID3v2.3 tags
3546         bool renamed;
3547         int storedFeatures = taggedFile->activeTaggedFileFeatures();
3548         taggedFile->setActiveTaggedFileFeatures(TaggedFile::TF_ID3v23);
3549         taggedFile->writeTags(true, &renamed,
3550                               FileConfig::instance().preserveTime());
3551         taggedFile->setActiveTaggedFileFeatures(storedFeatures);
3552         taggedFile->readTags(true);
3553       }
3554     }
3555   }
3556   emit selectedFilesUpdated();
3557 }
3558 
3559 /**
3560  * Get value of frame.
3561  * To get binary data like a picture, the name of a file to write can be
3562  * added after the @a name, e.g. "Picture:/path/to/file".
3563  *
3564  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
3565  * @param name    name of frame (e.g. "Artist")
3566  */
3567 QString Kid3Application::getFrame(Frame::TagVersion tagMask,
3568                                   const QString& name) const
3569 {
3570   QString frameName(name);
3571   QString dataFileName, fieldName;
3572   int index = 0;
3573   Frame::ExtendedType explicitType;
3574   if (frameName.startsWith(QLatin1Char('!'))) {
3575     frameName.remove(0, 1);
3576     explicitType = Frame::ExtendedType(Frame::FT_Other, frameName);
3577   }
3578   extractFileFieldIndex(frameName, dataFileName, fieldName, index);
3579   bool isRatingStars = false;
3580   if (frameName.toLower() == QLatin1String("ratingstars")) {
3581     frameName.truncate(6); // Reduce to "rating"
3582     isRatingStars = true;
3583   }
3584   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
3585   if (tagNr >= Frame::Tag_NumValues)
3586     return QString();
3587 
3588   FrameTableModel* ft = m_framesModel[tagNr];
3589   const FrameCollection& frames = ft->frames();
3590   if (auto it = explicitType.getType() == Frame::FT_UnknownFrame
3591                   ? frames.findByName(frameName, index)
3592                   : frames.findByExtendedType(explicitType, index);
3593       it != frames.cend()) {
3594     QString frmName(it->getName());
3595     if (!dataFileName.isEmpty() &&
3596         (tagMask & (Frame::TagV2 | Frame::TagV3)) != 0) {
3597       if (it->getType() == Frame::FT_Picture ||
3598           frmName.startsWith(QLatin1String("GEOB"))) {
3599         PictureFrame::writeDataToFile(*it, dataFileName);
3600       } else if (bool isSylt = false;
3601                  (isSylt = frmName.startsWith(QLatin1String("SYLT")) ||
3602                   frmName == QLatin1String("Chapters")) ||
3603                  frmName.startsWith(QLatin1String("ETCO"))) {
3604         QFile file(dataFileName);
3605         if (file.open(QIODevice::WriteOnly)) {
3606           TimeEventModel timeEventModel;
3607           if (isSylt) {
3608             timeEventModel.setType(TimeEventModel::SynchronizedLyrics);
3609             timeEventModel.fromSyltFrame(it->getFieldList());
3610           } else {
3611             timeEventModel.setType(TimeEventModel::EventTimingCodes);
3612             timeEventModel.fromEtcoFrame(it->getFieldList());
3613           }
3614           QTextStream stream(&file);
3615           if (QString codecName = FileConfig::instance().textEncoding();
3616               codecName != QLatin1String("System")) {
3617 #if QT_VERSION >= 0x060000
3618             if (auto encoding = QStringConverter::encodingForName(codecName.toLatin1())) {
3619               stream.setEncoding(*encoding);
3620             }
3621 #else
3622             stream.setCodec(codecName.toLatin1());
3623 #endif
3624           }
3625           timeEventModel.toLrcFile(stream, frames.getTitle(),
3626                                    frames.getArtist(), frames.getAlbum());
3627           file.close();
3628         }
3629       } else if (fieldName.isEmpty()) {
3630         it->writeValueToFile(dataFileName);
3631       }
3632     }
3633     if (!fieldName.isEmpty()) {
3634       if (fieldName == QLatin1String("selected")) {
3635         const int frameIndex = it->getIndex();
3636         if (const int row = frameIndex >= 0
3637                               ? ft->getRowWithFrameIndex(frameIndex)
3638                               : std::distance(frames.cbegin(), it);
3639             row != -1) {
3640           return QLatin1String(ft->index(row, FrameTableModel::CI_Enable)
3641                                .data(Qt::CheckStateRole).toInt() == Qt::Checked
3642                                ? "1" : "0");
3643         }
3644         return QString();
3645       }
3646       return Frame::getField(*it, fieldName).toString();
3647     }
3648     if (isRatingStars) {
3649       bool ok;
3650       int rating = it->getValue().toInt(&ok);
3651       if (ok) {
3652         return QString::number(TagConfig::instance().starCountFromRating(
3653                                  rating, ratingTypeName(*it)));
3654       }
3655     }
3656     return it->getValue();
3657   }
3658   return QString();
3659 }
3660 
3661 /**
3662  * Get names and values of all frames.
3663  *
3664  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
3665  *
3666  * @return map containing frame values.
3667  */
3668 QVariantMap Kid3Application::getAllFrames(Frame::TagVersion tagMask) const
3669 {
3670   QVariantMap map;
3671   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
3672   if (tagNr >= Frame::Tag_NumValues)
3673     return QVariantMap();
3674 
3675   FrameTableModel* ft = m_framesModel[tagNr];
3676   const FrameCollection& frames = ft->frames();
3677   for (auto it = frames.cbegin(); it != frames.cend(); ++it) {
3678     QString name(it->getName());
3679     if (int nlPos = name.indexOf(QLatin1Char('\n')); nlPos > 0) {
3680       // probably "TXXX - User defined text information\nDescription" or
3681       // "WXXX - User defined URL link\nDescription"
3682       name = name.mid(nlPos + 1);
3683 #if QT_VERSION >= 0x060000
3684     } else if (name.mid(4, 3) == QLatin1String(" - ")) {
3685 #else
3686     } else if (name.midRef(4, 3) == QLatin1String(" - ")) {
3687 #endif
3688       // probably "ID3-ID - Description"
3689       name = name.left(4);
3690     }
3691     map.insert(name, it->getValue());
3692   }
3693   return map;
3694 }
3695 
3696 /**
3697  * Set value of frame.
3698  * For tag 2 (@a tagMask 2), if no frame with @a name exists, a new frame
3699  * is added, if @a value is empty, the frame is deleted.
3700  * To add binary data like a picture, a file can be added after the
3701  * @a name, e.g. "Picture:/path/to/file".
3702  *
3703  * @param tagMask tag bit (1 for tag 1, 2 for tag 2)
3704  * @param name    name of frame (e.g. "Artist")
3705  * @param value   value of frame
3706  *
3707  * @return true if ok.
3708  */
3709 bool Kid3Application::setFrame(Frame::TagVersion tagMask,
3710                                const QString& name, const QString& value)
3711 {
3712   Frame::TagNumber tagNr = Frame::tagNumberFromMask(tagMask);
3713   if (tagNr >= Frame::Tag_NumValues)
3714     return false;
3715 
3716   FrameTableModel* ft = m_framesModel[tagNr];
3717   if (name == QLatin1String("*.selected")) {
3718     ft->setAllCheckStates(!value.isEmpty() && value != QLatin1String("0")
3719                                            && value != QLatin1String("false"));
3720     return true;
3721   }
3722 
3723   QString frameName(name);
3724   QString dataFileName, fieldName;
3725   int index = 0;
3726   Frame::ExtendedType explicitType;
3727   if (frameName.startsWith(QLatin1Char('!'))) {
3728     frameName.remove(0, 1);
3729     explicitType = Frame::ExtendedType(Frame::FT_Other, frameName);
3730   }
3731   extractFileFieldIndex(frameName, dataFileName, fieldName, index);
3732   bool isRatingStars = false;
3733   if (frameName.toLower() == QLatin1String("ratingstars")) {
3734     frameName.truncate(6); // Reduce to "rating"
3735     isRatingStars = true;
3736   }
3737   FrameCollection frames(ft->frames());
3738   if (auto it = explicitType.getType() == Frame::FT_UnknownFrame
3739                   ? frames.findByName(frameName, index)
3740                   : frames.findByExtendedType(explicitType, index);
3741       it != frames.end()) {
3742     QString frmName(it->getName());
3743     if (!dataFileName.isEmpty() &&
3744         (tagMask & (Frame::TagV2 | Frame::TagV3)) != 0) {
3745       if (it->getType() == Frame::FT_Picture) {
3746         deleteFrame(tagNr, frmName, index);
3747         PictureFrame frame;
3748         PictureFrame::setDescription(frame, value);
3749         PictureFrame::setDataFromFile(frame, dataFileName);
3750         PictureFrame::setMimeTypeFromFileName(frame, dataFileName);
3751         PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig());
3752         addFrame(tagNr, &frame);
3753       } else if (frmName.startsWith(QLatin1String("GEOB"))) {
3754         Frame frame(*it);
3755         deleteFrame(tagNr, frmName, index);
3756         Frame::setField(frame, Frame::ID_MimeType,
3757                         PictureFrame::getMimeTypeForFile(dataFileName));
3758         Frame::setField(frame, Frame::ID_Filename,
3759                         QFileInfo(dataFileName).fileName());
3760         Frame::setField(frame, Frame::ID_Description, value);
3761         PictureFrame::setDataFromFile(frame, dataFileName);
3762         addFrame(tagNr, &frame);
3763       } else if (bool isSylt = false;
3764                  (isSylt = frmName.startsWith(QLatin1String("SYLT")) ||
3765                   frmName == QLatin1String("Chapters")) ||
3766                  frmName.startsWith(QLatin1String("ETCO"))) {
3767         QFile file(dataFileName);
3768         if (file.open(QIODevice::ReadOnly)) {
3769           QTextStream stream(&file);
3770           Frame frame(*it);
3771           Frame::setField(frame, Frame::ID_Description, value);
3772           deleteFrame(tagNr, frmName, index);
3773           TimeEventModel timeEventModel;
3774           if (isSylt) {
3775             timeEventModel.setType(TimeEventModel::SynchronizedLyrics);
3776             timeEventModel.fromLrcFile(stream);
3777             timeEventModel.toSyltFrame(frame.fieldList());
3778           } else {
3779             timeEventModel.setType(TimeEventModel::EventTimingCodes);
3780             timeEventModel.fromLrcFile(stream);
3781             timeEventModel.toEtcoFrame(frame.fieldList());
3782           }
3783           file.close();
3784           addFrame(tagNr, &frame);
3785         }
3786       } else if (fieldName.isEmpty()) {
3787         auto& frame = const_cast<Frame&>(*it);
3788         frame.setValueFromFile(dataFileName);
3789         ft->transferFrames(frames);
3790         ft->selectChangedFrames();
3791         emit fileSelectionUpdateRequested();
3792         emit selectedFilesUpdated();
3793       }
3794     } else if (value.isEmpty() && fieldName.isEmpty() &&
3795                (tagMask & (Frame::TagV2 | Frame::TagV3)) != 0) {
3796       deleteFrame(tagNr, frmName, index);
3797     } else {
3798       auto& frame = const_cast<Frame&>(*it);
3799       if (fieldName.isEmpty()) {
3800         QString val(value);
3801         if (isRatingStars) {
3802           bool ok;
3803           if (int starCount = value.toInt(&ok);
3804               ok && starCount >= 0 && starCount <= 5) {
3805             val = QString::number(TagConfig::instance().starCountToRating(
3806                                     starCount, ratingTypeName(*it)));
3807           } else {
3808             return false;
3809           }
3810         }
3811         frame.setValueIfChanged(val);
3812       } else {
3813         if (fieldName == QLatin1String("selected")) {
3814           const int frameIndex = frame.getIndex();
3815           if (const int row = frameIndex >= 0
3816                                 ? ft->getRowWithFrameIndex(frameIndex)
3817                                 : std::distance(frames.cbegin(), it);
3818               row != -1) {
3819             ft->setData(ft->index(row, FrameTableModel::CI_Enable),
3820                         !value.isEmpty() && value != QLatin1String("0")
3821                                          && value != QLatin1String("false")
3822                         ? Qt::Checked : Qt::Unchecked, Qt::CheckStateRole);
3823             return true;
3824           }
3825         } else {
3826           if (TaggedFile* taggedFile = getSelectedFile();
3827               taggedFile && Frame::setField(frame, fieldName, value)) {
3828             taggedFile->setFrame(tagNr, frame);
3829           }
3830         }
3831       }
3832       ft->transferFrames(frames);
3833       ft->selectChangedFrames();
3834       emit fileSelectionUpdateRequested();
3835       emit selectedFilesUpdated();
3836     }
3837     return true;
3838   }
3839   if (tagMask & (Frame::TagV2 | Frame::TagV3)) {
3840     Frame frame(explicitType.getType() == Frame::FT_UnknownFrame
3841                   ? Frame::ExtendedType(frameName) : explicitType, value, -1);
3842     QString frmName(frame.getInternalName());
3843     if (!dataFileName.isEmpty()) {
3844       if (frame.getType() == Frame::FT_Picture) {
3845         PictureFrame::setFields(frame);
3846         PictureFrame::setDescription(frame, value);
3847         PictureFrame::setDataFromFile(frame, dataFileName);
3848         PictureFrame::setMimeTypeFromFileName(frame, dataFileName);
3849         PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig());
3850       } else if (frmName.startsWith(QLatin1String("GEOB"))) {
3851         PictureFrame::setGeobFields(
3852           frame, Frame::TE_ISO8859_1,
3853           PictureFrame::getMimeTypeForFile(dataFileName),
3854           QFileInfo(dataFileName).fileName(), value);
3855         PictureFrame::setDataFromFile(frame, dataFileName);
3856       } else if (bool isSylt = false;
3857                  (isSylt = frmName.startsWith(QLatin1String("SYLT")) ||
3858                    frmName == QLatin1String("Chapters")) ||
3859                  frmName.startsWith(QLatin1String("ETCO"))) {
3860         QFile file(dataFileName);
3861         if (file.open(QIODevice::ReadOnly)) {
3862           Frame::Field field;
3863           Frame::FieldList& fields = frame.fieldList();
3864           fields.clear();
3865           field.m_id = Frame::ID_Description;
3866           field.m_value = value;
3867           fields.append(field);
3868           field.m_id = Frame::ID_Data;
3869 #if QT_VERSION >= 0x060000
3870           field.m_value = QVariant(QMetaType(QMetaType::QVariantList));
3871 #else
3872           field.m_value = QVariant(QVariant::List);
3873 #endif
3874           fields.append(field);
3875           QTextStream stream(&file);
3876           TimeEventModel timeEventModel;
3877           if (isSylt) {
3878             timeEventModel.setType(TimeEventModel::SynchronizedLyrics);
3879             timeEventModel.fromLrcFile(stream);
3880             timeEventModel.toSyltFrame(frame.fieldList());
3881           } else {
3882             timeEventModel.setType(TimeEventModel::EventTimingCodes);
3883             timeEventModel.fromLrcFile(stream);
3884             timeEventModel.toEtcoFrame(frame.fieldList());
3885           }
3886           file.close();
3887         }
3888       } else if (fieldName.isEmpty()) {
3889         frame.setValueFromFile(dataFileName);
3890       }
3891     } else if (value.isEmpty()) {
3892       // Do not add an empty frame
3893       return false;
3894     }
3895     if (!fieldName.isEmpty()) {
3896       if (TaggedFile* taggedFile = getSelectedFile()) {
3897         frame.setValue(QString());
3898         taggedFile->addFieldList(tagNr, frame);
3899         if (!Frame::setField(frame, fieldName, value)) {
3900           return false;
3901         }
3902       }
3903     }
3904     if (isRatingStars) {
3905       bool ok;
3906       if (int starCount = value.toInt(&ok);
3907         ok && starCount >= 0 && starCount <= 5) {
3908         frame.setValue(QString::number(TagConfig::instance().starCountToRating(
3909           starCount, ratingTypeName(frame, getSelectedFile(), tagNr))));
3910       } else {
3911         return false;
3912       }
3913     }
3914     addFrame(tagNr, &frame);
3915     return true;
3916   }
3917   return false;
3918 }
3919 
3920 /**
3921  * Get data from picture frame.
3922  * @return picture data, empty if not found.
3923  */
3924 QByteArray Kid3Application::getPictureData() const
3925 {
3926   QByteArray data;
3927   const FrameCollection& frames = m_framesModel[Frame::Tag_Picture]->frames();
3928   if (auto it = frames.findByExtendedType(
3929         Frame::ExtendedType(Frame::FT_Picture));
3930       it != frames.cend()) {
3931     PictureFrame::getData(*it, data);
3932   }
3933   return data;
3934 }
3935 
3936 /**
3937  * Set data in picture frame.
3938  * @param data picture data
3939  */
3940 void Kid3Application::setPictureData(const QByteArray& data)
3941 {
3942   const FrameCollection& frames = m_framesModel[Frame::Tag_Picture]->frames();
3943   auto it = frames.findByExtendedType(
3944         Frame::ExtendedType(Frame::FT_Picture));
3945   PictureFrame frame;
3946   if (it != frames.cend()) {
3947     frame = PictureFrame(*it);
3948     deleteFrame(Frame::Tag_Picture, QLatin1String("Picture"));
3949   }
3950   if (!data.isEmpty()) {
3951     PictureFrame::setData(frame, data);
3952     PictureFrame::setTextEncoding(frame, frameTextEncodingFromConfig());
3953     addFrame(Frame::Tag_Picture, &frame);
3954   }
3955 }
3956 
3957 /**
3958  * Update state when file is about to be played.
3959  * @param filePath path to file
3960  */
3961 void Kid3Application::onAboutToPlay(const QString& filePath)
3962 {
3963 #ifdef Q_OS_WIN32
3964   // Phonon on Windows cannot play if the file is open.
3965   closeFileHandle(filePath);
3966 #endif
3967   if (GuiConfig::instance().selectFileOnPlayEnabled()) {
3968     selectFile(filePath);
3969   }
3970 }
3971 
3972 /**
3973  * Close the file handle of a tagged file.
3974  * @param filePath path to file
3975  */
3976 void Kid3Application::closeFileHandle(const QString& filePath)
3977 {
3978   if (QModelIndex index = m_fileProxyModel->index(filePath); index.isValid()) {
3979    if (TaggedFile* taggedFile = FileProxyModel::getTaggedFileOfIndex(index)) {
3980      taggedFile->closeFileHandle();
3981    }
3982  }
3983 }
3984 
3985 /**
3986  * Set a frame editor object to act as the frame editor.
3987  * @param frameEditor frame editor object, null to disable
3988  */
3989 void Kid3Application::setFrameEditor(FrameEditorObject* frameEditor)
3990 {
3991   if (m_frameEditor != frameEditor) {
3992     IFrameEditor* editor;
3993     bool storeCurrentEditor = false;
3994     if (frameEditor) {
3995       if (!m_frameEditor) {
3996         storeCurrentEditor = true;
3997       }
3998       editor = frameEditor;
3999     } else {
4000       editor = m_storedFrameEditor;
4001     }
4002     FOR_ALL_TAGS(tagNr) {
4003       if (tagNr != Frame::Tag_Id3v1) {
4004         FrameList* framelist = m_framelist[tagNr];
4005         if (storeCurrentEditor) {
4006           m_storedFrameEditor = framelist->frameEditor();
4007           storeCurrentEditor = false;
4008         }
4009         framelist->setFrameEditor(editor);
4010       }
4011     }
4012     m_frameEditor = frameEditor;
4013     emit frameEditorChanged();
4014   }
4015 }
4016 
4017 /**
4018  * Remove frame editor.
4019  * Has to be called in the destructor of the frame editor to avoid a dangling
4020  * pointer to a deleted object.
4021  * @param frameEditor frame editor
4022  */
4023 void Kid3Application::removeFrameEditor(const IFrameEditor* frameEditor)
4024 {
4025   if (m_storedFrameEditor == frameEditor) {
4026     m_storedFrameEditor = nullptr;
4027   }
4028   if (m_frameEditor == frameEditor) {
4029     setFrameEditor(nullptr);
4030   }
4031 }
4032 
4033 /**
4034  * Get the numbers of the selected rows in a list suitable for scripting.
4035  * @return list with row numbers.
4036  */
4037 QVariantList Kid3Application::getFileSelectionRows() const
4038 {
4039   QVariantList rows;
4040   const auto indexes = m_fileSelectionModel->selectedRows();
4041   rows.reserve(indexes.size());
4042   for (const QModelIndex& index : indexes) {
4043     rows.append(index.row());
4044   }
4045   return rows;
4046 }
4047 
4048 /**
4049  * Set the file selection from a list of model indexes.
4050  * @param indexes list of model indexes suitable for scripting
4051  */
4052 void Kid3Application::setFileSelectionIndexes(const QVariantList& indexes)
4053 {
4054   QItemSelection selection;
4055   QModelIndex firstIndex;
4056   for (const QVariant& var : indexes) {
4057     QModelIndex index = var.toModelIndex();
4058     if (!firstIndex.isValid()) {
4059       firstIndex = index;
4060     }
4061     selection.select(index, index);
4062   }
4063   disconnect(m_fileSelectionModel,
4064              &QItemSelectionModel::selectionChanged,
4065              this, &Kid3Application::fileSelectionChanged);
4066   m_fileSelectionModel->select(selection,
4067                    QItemSelectionModel::Clear | QItemSelectionModel::Select |
4068                    QItemSelectionModel::Rows);
4069   if (firstIndex.isValid()) {
4070     m_fileSelectionModel->setCurrentIndex(firstIndex,
4071         QItemSelectionModel::Select | QItemSelectionModel::Rows);
4072   }
4073   connect(m_fileSelectionModel,
4074           &QItemSelectionModel::selectionChanged,
4075           this, &Kid3Application::fileSelectionChanged);
4076 }
4077 
4078 /**
4079  * Set the image provider.
4080  * @param imageProvider image provider
4081  */
4082 void Kid3Application::setImageProvider(ImageDataProvider* imageProvider) {
4083   m_imageProvider = imageProvider;
4084 }
4085 
4086 /**
4087  * If an image provider is used, update its picture and change the
4088  * coverArtImageId property if the picture of the selection changed.
4089  * This can be used to change a QML image.
4090  */
4091 void Kid3Application::updateCoverArtImageId()
4092 {
4093   // Only perform expensive picture operations if the signal is used
4094   // (when using a QML image provider).
4095   if (m_imageProvider &&
4096       receivers(SIGNAL(coverArtImageIdChanged(QString))) > 0) {
4097     setCoverArtImageData(m_selection->getPicture());
4098   }
4099 }
4100 
4101 /**
4102  * Set picture data for image provider.
4103  * @param picture picture data
4104  */
4105 void Kid3Application::setCoverArtImageData(const QByteArray& picture)
4106 {
4107   if (picture != m_imageProvider->getImageData()) {
4108     m_imageProvider->setImageData(picture);
4109     setNextCoverArtImageId();
4110     emit coverArtImageIdChanged(m_coverArtImageId);
4111   }
4112 }
4113 
4114 /**
4115  * Set the coverArtImageId property to a new value.
4116  * This can be used to trigger an update of QML images.
4117  */
4118 void Kid3Application::setNextCoverArtImageId() {
4119   static quint32 nr = 0;
4120   m_coverArtImageId = QString(QLatin1String("image://kid3/data/%1"))
4121       .arg(nr++, 8, 16, QLatin1Char('0'));
4122 }
4123 
4124 /**
4125  * Open a file select dialog to get a file name.
4126  * For script support, is only supported when a GUI is available.
4127  * @param caption dialog caption
4128  * @param dir working directory
4129  * @param filter file type filter
4130  * @param saveFile true to open a save file dialog
4131  * @return selected file, empty if canceled.
4132  */
4133 QString Kid3Application::selectFileName(const QString& caption, const QString& dir,
4134                                         const QString& filter, bool saveFile)
4135 {
4136   return saveFile
4137       ? m_platformTools->getSaveFileName(nullptr, caption, dir, filter, nullptr)
4138       : m_platformTools->getOpenFileName(nullptr, caption, dir, filter, nullptr);
4139 }
4140 
4141 /**
4142  * Open a file select dialog to get a directory name.
4143  * For script support, is only supported when a GUI is available.
4144  * @param caption dialog caption
4145  * @param dir working directory
4146  * @return selected directory, empty if canceled.
4147  */
4148 QString Kid3Application::selectDirName(const QString& caption,
4149                                        const QString& dir)
4150 {
4151   return m_platformTools->getExistingDirectory(nullptr, caption, dir);
4152 }
4153 
4154 /**
4155  * Check if application is running with a graphical user interface.
4156  * @return true if application has a GUI.
4157  */
4158 bool Kid3Application::hasGui() const
4159 {
4160   return m_platformTools->hasGui();
4161 }