diff --git a/filerenamer.cpp b/filerenamer.cpp index 1d240bd8..db3e06a7 100644 --- a/filerenamer.cpp +++ b/filerenamer.cpp @@ -1,1064 +1,1051 @@ /** * Copyright (C) 2004, 2007, 2009 Michael Pyne * Copyright (C) 2003 Frerich Raabe * Copyright (C) 2014 Arnold Dumas * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #include "filerenamer.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "tag.h" #include "filerenameroptions.h" #include "filehandle.h" #include "exampleoptions.h" #include "playlistitem.h" #include "playlist.h" // processEvents() #include "coverinfo.h" #include "juk_debug.h" class ConfirmationDialog : public QDialog { public: ConfirmationDialog(const QMap &files, QWidget *parent = nullptr) : QDialog(parent) { setModal(true); setWindowTitle(i18nc("warning about mass file rename", "Warning")); auto vboxLayout = new QVBoxLayout(this); auto hbox = new QWidget(this); auto hboxVLayout = new QVBoxLayout(hbox); vboxLayout->addWidget(hbox); QLabel *l = new QLabel(hbox); l->setPixmap(QIcon::fromTheme("dialog-warning").pixmap(KIconLoader::SizeLarge)); hboxVLayout->addWidget(l); l = new QLabel(i18n("You are about to rename the following files. " "Are you sure you want to continue?"), hbox); hboxVLayout->addWidget(l, 1); QTreeWidget *lv = new QTreeWidget(this); QStringList headers; headers << i18n("Original Name"); headers << i18n("New Name"); lv->setHeaderLabels(headers); lv->setRootIsDecorated(false); vboxLayout->addWidget(lv); auto buttonBox = new QDialogButtonBox(this); vboxLayout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); int lvHeight = 0; QMap::ConstIterator it = files.constBegin(); for(; it != files.constEnd(); ++it) { QTreeWidgetItem *item = new QTreeWidgetItem(lv); item->setText(0, it.key()); if (it.key() != it.value()) { item->setText(1, it.value()); } else { item->setText(1, i18n("No Change")); } lvHeight += lv->visualItemRect(item).height(); } lvHeight += lv->horizontalScrollBar()->height() + lv->header()->height(); lv->setMinimumHeight(qMin(lvHeight, 400)); resize(qMin(width(), 500), qMin(minimumHeight(), 400)); show(); } }; // // Implementation of ConfigCategoryReader // ConfigCategoryReader::ConfigCategoryReader() : CategoryReaderInterface(), m_currentItem(0) { KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); QList categoryOrder = config.readEntry("CategoryOrder", QList()); int categoryCount[NumTypes] = { 0 }; // Keep track of each category encountered. // Set a default: if(categoryOrder.isEmpty()) categoryOrder << Artist << Album << Title << Track; QList::ConstIterator catIt = categoryOrder.constBegin(); for(; catIt != categoryOrder.constEnd(); ++catIt) { int catCount = categoryCount[*catIt]++; TagType category = static_cast(*catIt); CategoryID catId(category, catCount); m_options[catId] = TagRenamerOptions(catId); m_categoryOrder << catId; } m_folderSeparators.fill(false, m_categoryOrder.count() - 1); QList checkedSeparators = config.readEntry("CheckedDirSeparators", QList()); QList::ConstIterator it = checkedSeparators.constBegin(); for(; it != checkedSeparators.constEnd(); ++it) { if(*it < m_folderSeparators.count()) m_folderSeparators[*it] = true; } m_musicFolder = config.readPathEntry("MusicFolder", "${HOME}/music"); m_separator = config.readEntry("Separator", " - "); } QString ConfigCategoryReader::categoryValue(TagType type) const { if(!m_currentItem) return QString(); Tag *tag = m_currentItem->file().tag(); switch(type) { case Track: return QString::number(tag->track()); case Year: return QString::number(tag->year()); case Title: return tag->title(); case Artist: return tag->artist(); case Album: return tag->album(); case Genre: return tag->genre(); default: return QString(); } } QString ConfigCategoryReader::prefix(const CategoryID &category) const { return m_options[category].prefix(); } QString ConfigCategoryReader::suffix(const CategoryID &category) const { return m_options[category].suffix(); } TagRenamerOptions::EmptyActions ConfigCategoryReader::emptyAction(const CategoryID &category) const { return m_options[category].emptyAction(); } QString ConfigCategoryReader::emptyText(const CategoryID &category) const { return m_options[category].emptyText(); } QList ConfigCategoryReader::categoryOrder() const { return m_categoryOrder; } QString ConfigCategoryReader::separator() const { return m_separator; } QString ConfigCategoryReader::musicFolder() const { return m_musicFolder; } int ConfigCategoryReader::trackWidth(int categoryNum) const { return m_options[CategoryID(Track, categoryNum)].trackWidth(); } bool ConfigCategoryReader::hasFolderSeparator(int index) const { if(index >= m_folderSeparators.count()) return false; return m_folderSeparators[index]; } bool ConfigCategoryReader::isDisabled(const CategoryID &category) const { return m_options[category].disabled(); } // // Implementation of FileRenamerWidget // FileRenamerWidget::FileRenamerWidget(QWidget *parent) : QWidget(parent), CategoryReaderInterface(), m_ui(new Ui::FileRenamerBase), m_exampleFromFile(false) { m_ui->setupUi(this); // This must be created before createTagRows() is called. m_exampleDialog = new ExampleOptionsDialog(this); createTagRows(); loadConfig(); // Add correct text to combo box. m_ui->m_category->clear(); for(int i = StartTag; i < NumTypes; ++i) { QString category = TagRenamerOptions::tagTypeText(static_cast(i)); m_ui->m_category->addItem(category); } - connect(m_exampleDialog, SIGNAL(signalShown()), SLOT(exampleDialogShown())); - connect(m_exampleDialog, SIGNAL(signalHidden()), SLOT(exampleDialogHidden())); - connect(m_exampleDialog, SIGNAL(dataChanged()), SLOT(dataSelected())); - connect(m_exampleDialog, SIGNAL(fileChanged(QString)), - this, SLOT(fileSelected(QString))); - connect(m_ui->dlgButtonBox, SIGNAL(accepted()), SIGNAL(accepted())); - connect(m_ui->dlgButtonBox, SIGNAL(rejected()), SIGNAL(rejected())); + connect(m_exampleDialog, &ExampleOptionsDialog::signalShown, + this, &FileRenamerWidget::exampleDialogShown); + connect(m_exampleDialog, &ExampleOptionsDialog::signalHidden, + this, &FileRenamerWidget::exampleDialogHidden); + connect(m_exampleDialog, &ExampleOptionsDialog::dataChanged, + this, &FileRenamerWidget::dataSelected); + connect(m_exampleDialog, &ExampleOptionsDialog::fileChanged, + this, &FileRenamerWidget::fileSelected); + connect(m_ui->dlgButtonBox, &QDialogButtonBox::accepted, this, [this]() { + emit accepted(); + }); + connect(m_ui->dlgButtonBox, &QDialogButtonBox::rejected, this, [this]() { + emit rejected(); + }); exampleTextChanged(); } void FileRenamerWidget::loadConfig() { QList checkedSeparators; KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); for(int i = 0; i < m_rows.count(); ++i) m_rows[i].options = TagRenamerOptions(m_rows[i].category); checkedSeparators = config.readEntry("CheckedDirSeparators", QList()); foreach(int separator, checkedSeparators) { if(separator < m_folderSwitches.count()) m_folderSwitches[separator]->setChecked(true); } QString path = config.readEntry("MusicFolder", "${HOME}/music"); m_ui->m_musicFolder->setUrl(QUrl::fromLocalFile(path)); m_ui->m_musicFolder->setMode(KFile::Directory | KFile::ExistingOnly | KFile::LocalOnly); m_ui->m_separator->setEditText(config.readEntry("Separator", " - ")); } void FileRenamerWidget::saveConfig() { KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); QList checkedSeparators; QList categoryOrder; for(int i = 0; i < m_rows.count(); ++i) { int rowId = idOfPosition(i); // Write out in GUI order, not m_rows order m_rows[rowId].options.saveConfig(m_rows[rowId].category.categoryNumber); categoryOrder += m_rows[rowId].category.category; } for(int i = 0; i < m_folderSwitches.count(); ++i) if(m_folderSwitches[i]->isChecked() == true) checkedSeparators += i; config.writeEntry("CheckedDirSeparators", checkedSeparators); config.writeEntry("CategoryOrder", categoryOrder); config.writePathEntry("MusicFolder", m_ui->m_musicFolder->url().path()); config.writeEntry("Separator", m_ui->m_separator->currentText()); config.sync(); } FileRenamerWidget::~FileRenamerWidget() { } int FileRenamerWidget::addRowCategory(TagType category) { static QIcon up = QIcon::fromTheme("go-up"); static QIcon down = QIcon::fromTheme("go-down"); // Find number of categories already of this type. int categoryCount = 0; - for(int i = 0; i < m_rows.count(); ++i) - if(m_rows[i].category.category == category) + for(const auto &row : m_rows) + if(row.category.category == category) ++categoryCount; Row row; row.category = CategoryID(category, categoryCount); row.position = m_rows.count(); QFrame *frame = new QFrame(m_mainFrame); QHBoxLayout *frameLayout = new QHBoxLayout(frame); frameLayout->setMargin(3); row.widget = frame; frame->setFrameShape(QFrame::Box); frame->setLineWidth(1); QBoxLayout *mainFrameLayout = static_cast(m_mainFrame->layout()); mainFrameLayout->addWidget(frame, 1); QFrame *buttons = new QFrame(frame); QVBoxLayout *buttonLayout = new QVBoxLayout(buttons); frameLayout->addWidget(buttons); buttons->setFrameStyle(QFrame::Plain | QFrame::Box); buttons->setLineWidth(1); row.upButton = new QPushButton(buttons); row.downButton = new QPushButton(buttons); row.upButton->setIcon(up); row.downButton->setIcon(down); row.upButton->setFlat(true); row.downButton->setFlat(true); buttonLayout->addWidget(row.upButton); buttonLayout->addWidget(row.downButton); QString labelText = QString("%1").arg(TagRenamerOptions::tagTypeText(category)); QLabel *label = new QLabel(labelText, frame); frameLayout->addWidget(label, 1); label->setAlignment(Qt::AlignCenter); QVBoxLayout *optionLayout = new QVBoxLayout; frameLayout->addLayout(optionLayout); row.enableButton = new QPushButton(i18nc("remove music genre from file renamer", "Remove"), frame); optionLayout->addWidget(row.enableButton); row.optionsButton = new QPushButton(i18nc("file renamer genre options", "Options"), frame); optionLayout->addWidget(row.optionsButton); row.widget->show(); m_rows.append(row); assignPositionHandlerForRow(row); // Disable add button if there's too many rows. if(m_rows.count() == MAX_CATEGORIES) m_ui->m_insertCategory->setEnabled(false); return row.position; } void FileRenamerWidget::assignPositionHandlerForRow(Row &row) { const auto id = row.position; disconnect(row.upButton); disconnect(row.downButton); disconnect(row.enableButton); disconnect(row.optionsButton); connect(row.upButton, &QPushButton::clicked, this, [this, id]() { this->moveItemUp(id); }); connect(row.downButton, &QPushButton::clicked, this, [this, id]() { this->moveItemDown(id); }); connect(row.enableButton, &QPushButton::clicked, this, [this, id]() { this->slotRemoveRow(id); }); connect(row.optionsButton, &QPushButton::clicked, this, [this, id]() { this->showCategoryOption(id); }); } bool FileRenamerWidget::removeRow(int id) { if(id >= m_rows.count()) { qCWarning(JUK_LOG) << "Trying to remove row, but " << id << " is out-of-range.\n"; return false; } if(m_rows.count() == 1) { qCCritical(JUK_LOG) << "Can't remove last row of File Renamer.\n"; return false; } delete m_rows[id].widget; m_rows[id].widget = nullptr; m_rows[id].enableButton = nullptr; m_rows[id].upButton = nullptr; m_rows[id].optionsButton = nullptr; m_rows[id].downButton = nullptr; int checkboxPosition = 0; // Remove first checkbox. // If not the first row, remove the checkbox before it. if(m_rows[id].position > 0) checkboxPosition = m_rows[id].position - 1; // The checkbox is contained within a layout widget, so the layout // widget is the one the needs to die. delete m_folderSwitches[checkboxPosition]->parent(); m_folderSwitches.erase(&m_folderSwitches[checkboxPosition]); // Go through all the rows and if they have the same category and a // higher categoryNumber, decrement the number. Also update the // position identifier. for(int i = 0; i < m_rows.count(); ++i) { if(i == id) continue; // Don't mess with ourself. if((m_rows[id].category.category == m_rows[i].category.category) && (m_rows[id].category.categoryNumber < m_rows[i].category.categoryNumber)) { --m_rows[i].category.categoryNumber; } // Items are moving up. if(m_rows[id].position < m_rows[i].position) --m_rows[i].position; } // Every row after the one we delete will have a different identifier, since // the identifier is simply its index into m_rows. So we need to re-do the // signal mappings for the affected rows after updating its position. for(int i = id + 1; i < m_rows.count(); ++i) assignPositionHandlerForRow(m_rows[i]); m_rows.erase(&m_rows[id]); // Make sure we update the buttons of affected rows. m_rows[idOfPosition(0)].upButton->setEnabled(false); m_rows[idOfPosition(m_rows.count() - 1)].downButton->setEnabled(false); // We can insert another row now, make sure GUI is updated to match. m_ui->m_insertCategory->setEnabled(true); - QTimer::singleShot(0, this, SLOT(exampleTextChanged())); + QTimer::singleShot(0, this, &FileRenamerWidget::exampleTextChanged); return true; } void FileRenamerWidget::addFolderSeparatorCheckbox() { QWidget *temp = new QWidget(m_mainFrame); m_mainFrame->layout()->addWidget(temp); QHBoxLayout *l = new QHBoxLayout(temp); QCheckBox *cb = new QCheckBox(i18n("Insert folder separator"), temp); m_folderSwitches.append(cb); l->addWidget(cb, 0, Qt::AlignCenter); cb->setChecked(false); - connect(cb, SIGNAL(toggled(bool)), - SLOT(exampleTextChanged())); + connect(cb, &QCheckBox::toggled, this, &FileRenamerWidget::exampleTextChanged); temp->show(); } void FileRenamerWidget::createTagRows() { KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer"); QList categoryOrder = config.readEntry("CategoryOrder", QList()); if(categoryOrder.isEmpty()) categoryOrder << Artist << Album << Title << Track; // Setup arrays. m_rows.reserve(categoryOrder.count()); m_folderSwitches.reserve(categoryOrder.count() - 1); m_mainFrame = new QFrame(m_ui->m_mainView); m_ui->m_mainView->setWidget(m_mainFrame); m_ui->m_mainView->setWidgetResizable(true); QVBoxLayout *frameLayout = new QVBoxLayout(m_mainFrame); frameLayout->setMargin(10); frameLayout->setSpacing(5); // OK, the deal with the categoryOrder variable is that we need to create // the rows in the order that they were saved in (the order given by categoryOrder). // The signal mappers operate according to the row identifier. To find the position of // a row given the identifier, use m_rows[id].position. To find the id of a given // position, use idOfPosition(position). - QList::ConstIterator it = categoryOrder.constBegin(); - - for(; it != categoryOrder.constEnd(); ++it) { + for(auto it = categoryOrder.cbegin(); it != categoryOrder.cend(); ++it) { if(*it < StartTag || *it >= NumTypes) { qCCritical(JUK_LOG) << "Invalid category encountered in file renamer configuration.\n"; continue; } if(m_rows.count() == MAX_CATEGORIES) { qCCritical(JUK_LOG) << "Maximum number of File Renamer tags reached, bailing.\n"; break; } - TagType i = static_cast(*it); - - addRowCategory(i); + addRowCategory(static_cast(*it)); // Insert the directory separator checkbox if this isn't the last // item. - QList::ConstIterator dup(it); - - // Check for last item - if(++dup != categoryOrder.constEnd()) + if((it + 1) != categoryOrder.constEnd()) addFolderSeparatorCheckbox(); } m_rows.first().upButton->setEnabled(false); m_rows.last().downButton->setEnabled(false); // If we have maximum number of categories already, don't let the user // add more. if(m_rows.count() >= MAX_CATEGORIES) m_ui->m_insertCategory->setEnabled(false); } void FileRenamerWidget::exampleTextChanged() { // Just use .mp3 as an example if(m_exampleFromFile && (m_exampleFile.isEmpty() || !FileHandle(m_exampleFile).tag()->isValid())) { m_ui->m_exampleText->setText(i18n("No file selected, or selected file has no tags.")); return; } m_ui->m_exampleText->setText(FileRenamer::fileName(*this) + ".mp3"); } QString FileRenamerWidget::fileCategoryValue(TagType category) const { FileHandle file(m_exampleFile); Tag *tag = file.tag(); switch(category) { case Track: return QString::number(tag->track()); case Year: return QString::number(tag->year()); case Title: return tag->title(); case Artist: return tag->artist(); case Album: return tag->album(); case Genre: return tag->genre(); default: return QString(); } } QString FileRenamerWidget::categoryValue(TagType category) const { if(m_exampleFromFile) return fileCategoryValue(category); const ExampleOptions *example = m_exampleDialog->widget(); switch (category) { case Track: return example->m_exampleTrack->text(); case Year: return example->m_exampleYear->text(); case Title: return example->m_exampleTitle->text(); case Artist: return example->m_exampleArtist->text(); case Album: return example->m_exampleAlbum->text(); case Genre: return example->m_exampleGenre->text(); default: return QString(); } } QList FileRenamerWidget::categoryOrder() const { QList list; // Iterate in GUI row order. for(int i = 0; i < m_rows.count(); ++i) { int rowId = idOfPosition(i); list += m_rows[rowId].category; } return list; } bool FileRenamerWidget::hasFolderSeparator(int index) const { if(index >= m_folderSwitches.count()) return false; return m_folderSwitches[index]->isChecked(); } void FileRenamerWidget::moveItem(int id, MovementDirection direction) { QWidget *l = m_rows[id].widget; int bottom = m_rows.count() - 1; int pos = m_rows[id].position; int newPos = (direction == MoveUp) ? pos - 1 : pos + 1; // Item we're moving can't go further down after this. if((pos == (bottom - 1) && direction == MoveDown) || (pos == bottom && direction == MoveUp)) { int idBottomRow = idOfPosition(bottom); int idAboveBottomRow = idOfPosition(bottom - 1); m_rows[idBottomRow].downButton->setEnabled(true); m_rows[idAboveBottomRow].downButton->setEnabled(false); } // We're moving the top item, do some button switching. if((pos == 0 && direction == MoveDown) || (pos == 1 && direction == MoveUp)) { int idTopItem = idOfPosition(0); int idBelowTopItem = idOfPosition(1); m_rows[idTopItem].upButton->setEnabled(true); m_rows[idBelowTopItem].upButton->setEnabled(false); } // This is the item we're swapping with. int idSwitchWith = idOfPosition(newPos); QWidget *w = m_rows[idSwitchWith].widget; // Update the table of widget rows. std::swap(m_rows[id].position, m_rows[idSwitchWith].position); // Move the item two spaces above/below its previous position. It has to // be 2 spaces because of the checkbox. QBoxLayout *layout = dynamic_cast(m_mainFrame->layout()); if ( !layout ) return; layout->removeWidget(l); layout->insertWidget(2 * newPos, l); // Move the top item two spaces in the opposite direction, for a similar // reason. layout->removeWidget(w); layout->insertWidget(2 * pos, w); layout->invalidate(); - QTimer::singleShot(0, this, SLOT(exampleTextChanged())); + QTimer::singleShot(0, this, &FileRenamerWidget::exampleTextChanged); } int FileRenamerWidget::idOfPosition(int position) const { if(position >= m_rows.count()) { qCCritical(JUK_LOG) << "Search for position " << position << " out-of-range.\n"; return -1; } for(int i = 0; i < m_rows.count(); ++i) if(m_rows[i].position == position) return i; qCCritical(JUK_LOG) << "Unable to find identifier for position " << position; return -1; } int FileRenamerWidget::findIdentifier(const CategoryID &category) const { for(int index = 0; index < m_rows.count(); ++index) if(m_rows[index].category == category) return index; qCCritical(JUK_LOG) << "Unable to find match for category " << TagRenamerOptions::tagTypeText(category.category) << ", number " << category.categoryNumber; return MAX_CATEGORIES; } -void FileRenamerWidget::enableAllUpButtons() -{ - for(int i = 0; i < m_rows.count(); ++i) - m_rows[i].upButton->setEnabled(true); -} - -void FileRenamerWidget::enableAllDownButtons() -{ - for(int i = 0; i < m_rows.count(); ++i) - m_rows[i].downButton->setEnabled(true); -} - void FileRenamerWidget::showCategoryOption(int id) { TagOptionsDialog *dialog = new TagOptionsDialog(this, m_rows[id].options, m_rows[id].category.categoryNumber); if(dialog->exec() == QDialog::Accepted) { m_rows[id].options = dialog->options(); exampleTextChanged(); } delete dialog; } void FileRenamerWidget::moveItemUp(int id) { moveItem(id, MoveUp); } void FileRenamerWidget::moveItemDown(int id) { moveItem(id, MoveDown); } void FileRenamerWidget::toggleExampleDialog() { m_exampleDialog->setHidden(!m_exampleDialog->isHidden()); } void FileRenamerWidget::insertCategory() { TagType category = static_cast(m_ui->m_category->currentIndex()); if(m_ui->m_category->currentIndex() < 0 || category >= NumTypes) { qCCritical(JUK_LOG) << "Trying to add unknown category somehow.\n"; return; } // We need to enable the down button of the current bottom row since it // can now move down. int idBottom = idOfPosition(m_rows.count() - 1); m_rows[idBottom].downButton->setEnabled(true); addFolderSeparatorCheckbox(); // Identifier of new row. int id = addRowCategory(category); // Set its down button to be disabled. m_rows[id].downButton->setEnabled(false); m_mainFrame->layout()->invalidate(); m_ui->m_mainView->update(); // Now update according to the code in loadConfig(). m_rows[id].options = TagRenamerOptions(m_rows[id].category); exampleTextChanged(); } void FileRenamerWidget::exampleDialogShown() { m_ui->m_showExample->setText(i18n("Hide Renamer Test Dialog")); } void FileRenamerWidget::exampleDialogHidden() { m_ui->m_showExample->setText(i18n("Show Renamer Test Dialog")); } void FileRenamerWidget::fileSelected(const QString &file) { m_exampleFromFile = true; m_exampleFile = file; exampleTextChanged(); } void FileRenamerWidget::dataSelected() { m_exampleFromFile = false; exampleTextChanged(); } QString FileRenamerWidget::separator() const { return m_ui->m_separator->currentText(); } QString FileRenamerWidget::musicFolder() const { return m_ui->m_musicFolder->url().path(); } void FileRenamerWidget::slotRemoveRow(int id) { // Remove the given identified row. if(!removeRow(id)) qCCritical(JUK_LOG) << "Unable to remove row " << id; } // // Implementation of FileRenamer // FileRenamer::FileRenamer() { } void FileRenamer::rename(PlaylistItem *item) { PlaylistItemList list; list.append(item); rename(list); } void FileRenamer::rename(const PlaylistItemList &items) { ConfigCategoryReader reader; QStringList errorFiles; QMap map; QMap itemMap; for(PlaylistItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) { reader.setPlaylistItem(*it); QString oldFile = (*it)->file().absFilePath(); QString extension = (*it)->file().fileInfo().suffix(); QString newFile = fileName(reader) + '.' + extension; if(oldFile != newFile) { map[oldFile] = newFile; itemMap[oldFile] = *it; } } if(itemMap.isEmpty() || ConfirmationDialog(map).exec() != QDialog::Accepted) return; QApplication::setOverrideCursor(Qt::WaitCursor); for(QMap::ConstIterator it = map.constBegin(); it != map.constEnd(); ++it) { if(moveFile(it.key(), it.value())) { itemMap[it.key()]->setFile(it.value()); itemMap[it.key()]->refresh(); setFolderIcon(QUrl::fromLocalFile(it.value()), itemMap[it.key()]); } else errorFiles << i18n("%1 to %2", it.key(), it.value()); processEvents(); } QApplication::restoreOverrideCursor(); if(!errorFiles.isEmpty()) KMessageBox::errorList(0, i18n("The following rename operations failed:\n"), errorFiles); } bool FileRenamer::moveFile(const QString &src, const QString &dest) { qCDebug(JUK_LOG) << "Moving file " << src << " to " << dest; QUrl srcURL = QUrl::fromLocalFile(src); QUrl dstURL = QUrl::fromLocalFile(dest); if(!srcURL.isValid() || !dstURL.isValid() || srcURL == dstURL) return false; QUrl dir = dstURL.resolved(QUrl::fromUserInput(".")); // resolves to path w/out filename if(!QDir().mkpath(dir.path())) { qCCritical(JUK_LOG) << "Unable to create directory " << dir.path(); return false; } // Move the file. KIO::Job *job = KIO::file_move(srcURL, dstURL); return job->exec(); } void FileRenamer::setFolderIcon(const QUrl &dstURL, const PlaylistItem *item) { if(item->file().tag()->album().isEmpty() || !item->file().coverInfo()->hasCover()) { return; } // Split path, and go through each path element. If a path element has // the album information, set its folder icon. QStringList elements = dstURL.path().split('/', QString::SkipEmptyParts); QString path; for(QStringList::ConstIterator it = elements.constBegin(); it != elements.constEnd(); ++it) { path.append('/' + (*it)); qCDebug(JUK_LOG) << "Checking path: " << path; if((*it).contains(item->file().tag()->album()) && QDir(path).exists() && !QFile::exists(path + "/.directory")) { // Seems to be a match, let's set the folder icon for the current // path. First we should write out the file. QPixmap thumb = item->file().coverInfo()->pixmap(CoverInfo::Thumbnail); thumb.save(path + "/.juk-thumbnail.png", "PNG"); KDesktopFile dirFile(path + "/.directory"); KConfigGroup desktopGroup(dirFile.desktopGroup()); if(!desktopGroup.hasKey("Icon")) { desktopGroup.writePathEntry("Icon", QString("%1/.juk-thumbnail.png").arg(path)); dirFile.sync(); } return; } } } /** * Returns iterator pointing to the last item enabled in the given list with * a non-empty value (or is required to be included). */ QList::ConstIterator lastEnabledItem(const QList &list, const CategoryReaderInterface &interface) { QList::ConstIterator it = list.constBegin(); QList::ConstIterator last = list.constEnd(); for(; it != list.constEnd(); ++it) { if(interface.isRequired(*it) || (!interface.isDisabled(*it) && !interface.categoryValue((*it).category).isEmpty())) { last = it; } } return last; } QString FileRenamer::fileName(const CategoryReaderInterface &interface) { const QList categoryOrder = interface.categoryOrder(); const QString separator = interface.separator(); const QString folder = interface.musicFolder(); QList::ConstIterator lastEnabled; int i = 0; QStringList list; QChar dirSeparator (QDir::separator()); // Use lastEnabled to properly handle folder separators. lastEnabled = lastEnabledItem(categoryOrder, interface); bool pastLast = false; // Toggles to true once we've passed lastEnabled. for(QList::ConstIterator it = categoryOrder.constBegin(); it != categoryOrder.constEnd(); ++it, ++i) { if(it == lastEnabled) pastLast = true; if(interface.isDisabled(*it)) continue; QString value = interface.value(*it); // The user can use the folder separator checkbox to add folders, so don't allow // slashes that slip in to accidentally create new folders. Should we filter this // back out when showing it in the GUI? value.replace('/', "%2f"); if(!pastLast && interface.hasFolderSeparator(i)) value.append(dirSeparator); if(interface.isRequired(*it) || !value.isEmpty()) list.append(value); } // Construct a single string representation, handling strings ending in // '/' specially QString result; for(QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); /* Empty */) { result += *it; ++it; // Manually advance iterator to check for end-of-list. // Add separator unless at a directory boundary if(it != list.constEnd() && !(*it).startsWith(dirSeparator) && // Check beginning of next item. !result.endsWith(dirSeparator)) { result += separator; } } return QString(folder + dirSeparator + result); } // vim: set et sw=4 tw=0 sta: diff --git a/filerenamer.h b/filerenamer.h index b4d6b518..e688903e 100644 --- a/filerenamer.h +++ b/filerenamer.h @@ -1,537 +1,519 @@ /** * Copyright (C) 2004, 2007 Michael Pyne * Copyright (C) 2003 Frerich Raabe * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later * version. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A * PARTICULAR PURPOSE. See the GNU General Public License for more details. * * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ #ifndef JUK_FILERENAMER_H #define JUK_FILERENAMER_H #include #include #include #include "ui_filerenamerbase.h" #include "categoryreaderinterface.h" #include "tagrenameroptions.h" class QCheckBox; class QPushButton; class QUrl; class ExampleOptionsDialog; class PlaylistItem; typedef QVector PlaylistItemList; // Used to decide what direction the FileRenamerWidget will move rows in. enum MovementDirection { MoveUp, MoveDown }; /** * This is used by FileRenamerWidget to store information about a particular * tag type, including its position, the QFrame holding the information, * the up, down, and enable buttons, and the user-selected renaming options. */ -struct Row +struct Row final { Row() : widget(0), upButton(0), downButton(0), enableButton(0) {} QWidget *widget; QPushButton *upButton, *downButton, *optionsButton, *enableButton; TagRenamerOptions options; CategoryID category; // Includes category and a disambiguation id. int position; ///< Position in the GUI (0 == top) QString name; }; /** * A list of rows, each of which may have its own category options and other * associated data. There is no relation between the order of rows in the vector and their * GUI layout. Instead, each Row has a position member which indicates what GUI position it * takes up. The index into the vector is known as the row identifier (which is unique but * not necessarily constant). */ typedef QVector Rows; -/** - * Holds a list directory separator checkboxes which separate a row. There - * should always be 1 less than the number of rows in the GUI. - * - * Used for ConfigCategoryReader. - */ -typedef QVector DirSeparatorCheckBoxes; - /** * Associates a CategoryID combination with a set of options. * * Used for ConfigCategoryReader */ typedef QMap CategoryOptionsMap; /** * An implementation of CategoryReaderInterface that reads the user's settings * from the global KConfig configuration object, and reads track information * from whatever the given PlaylistItem is. You can assign different * PlaylistItems in order to change the returned tag category information. * * @author Michael Pyne */ -class ConfigCategoryReader : public CategoryReaderInterface +class ConfigCategoryReader final : public CategoryReaderInterface { public: // ConfigCategoryReader specific members ConfigCategoryReader(); const PlaylistItem *playlistItem() const { return m_currentItem; } void setPlaylistItem(const PlaylistItem *item) { m_currentItem = item; } // CategoryReaderInterface reimplementations virtual QString categoryValue(TagType type) const override; virtual QString prefix(const CategoryID &category) const override; virtual QString suffix(const CategoryID &category) const override; virtual TagRenamerOptions::EmptyActions emptyAction(const CategoryID &category) const override; virtual QString emptyText(const CategoryID &category) const override; virtual QList categoryOrder() const override; virtual QString separator() const override; virtual QString musicFolder() const override; virtual int trackWidth(int categoryNum) const override; virtual bool hasFolderSeparator(int index) const override; virtual bool isDisabled(const CategoryID &category) const override; private: const PlaylistItem *m_currentItem; CategoryOptionsMap m_options; QList m_categoryOrder; QString m_separator; QString m_musicFolder; QVector m_folderSeparators; }; /** * This class implements a dialog that allows the user to alter the behavior * of the file renamer. It supports 6 different genre types at this point, * and it shouldn't be too difficult to extend that in the future if needed. * It allows the user to open an external dialog, which will let the user see * an example of what their current options will look like, by either allowing * the user to type in some sample information, or by loading a file and * reading tags from there. * * It also implements the CategoryReaderInterface in order to implement the * example filename functionality. * * @author Michael Pyne */ -class FileRenamerWidget : public QWidget, public CategoryReaderInterface +class FileRenamerWidget final : public QWidget, public CategoryReaderInterface { Q_OBJECT public: explicit FileRenamerWidget(QWidget *parent); ~FileRenamerWidget(); /// Maximum number of total categories the widget will allow. static int const MAX_CATEGORIES = 16; /** * This function saves all of the category options to the global KConfig * object. You must call this manually, FileRenamerWidget doesn't call it * automatically so that situations where the user hits "Cancel" work * correctly. */ void saveConfig(); signals: void accepted(); // for the QDialogButtonBox void rejected(); protected slots: /** * This function should be called whenever the example text may need to be * changed. For example, when the user selects a different separator or * changes the example text, this slot should be called. */ virtual void exampleTextChanged(); /** * This function shows the example dialog if it is hidden, and hides the * example dialog if it is shown. */ virtual void toggleExampleDialog(); /** * This function inserts the currently selected category, so that the * user can use duplicate tags in the file renamer. */ virtual void insertCategory(); private: /** * This function initializes the category options by loading the data from * the global KConfig object. This is called automatically in the constructor. */ void loadConfig(); /** * This function adds a "Insert Folder separator" checkbox to the end of * the current layout. The setting defaults to being unchecked. */ void addFolderSeparatorCheckbox(); /** * This function creates a row in the main view for category, appending it * to the end. It handles connecting signals to the mapper and such as * well. * * @param category Type of row to append. * @return identifier of newly added row. */ int addRowCategory(TagType category); /** * Removes the given row, updating the other rows to have the correct * number of categoryNumber. * * @param id The identifier of the row to remove. * @return true if the delete succeeded, false otherwise. */ bool removeRow(int id); /** * Installs button signal handlers for the buttons in @p row so that they * are called in response to GUI events, and removes any existing handlers. */ void assignPositionHandlerForRow(Row &row); /** * This function sets up the internal view by creating the checkboxes and * the rows for each category. */ void createTagRows(); /** * Returns the value for \p category by retrieving the tag from m_exampleFile. * If \p category is Track, then an appropriate fixup will be applied if needed * to match the user's desired minimum width. * * @param category the category to retrieve the value for. * @return the string representation of the value for \p category. */ QString fileCategoryValue(TagType category) const; /** * Returns the value for \p category by reading the user entry for that * category. If \p category is Track, then an appropriate fixup will be applied * if needed to match the user's desired minimum width. * * @param category the category to retrieve the value for. * @return the string representation of the value for \p category. */ virtual QString categoryValue(TagType category) const override; /** * Returns the user-specified prefix string for \p category. * * @param category the category to retrieve the value for. * @return user-specified prefix string for \p category. */ virtual QString prefix(const CategoryID &category) const override { return m_rows[findIdentifier(category)].options.prefix(); } /** * Returns the user-specified suffix string for \p category. * * @param category the category to retrieve the value for. * @return user-specified suffix string for \p category. */ virtual QString suffix(const CategoryID &category) const override { return m_rows[findIdentifier(category)].options.suffix(); } /** * Returns the user-specified empty action for \p category. * * @param category the category to retrieve the value for. * @return user-specified empty action for \p category. */ virtual TagRenamerOptions::EmptyActions emptyAction(const CategoryID &category) const override { return m_rows[findIdentifier(category)].options.emptyAction(); } /** * Returns the user-specified empty text for \p category. This text might * be used to replace an empty value. * * @param category the category to retrieve the value for. * @return the user-specified empty text for \p category. */ virtual QString emptyText(const CategoryID &category) const override { return m_rows[findIdentifier(category)].options.emptyText(); } /** * @return list of CategoryIDs corresponding to the user-specified category order. */ virtual QList categoryOrder() const override; /** * @return string that separates the tag values in the file name. */ virtual QString separator() const override; /** * @return local path to the music folder used to store renamed files. */ virtual QString musicFolder() const override; /** * @param categoryNum Zero-based number of category to get results for (if more than one). * @return the minimum width of the track category. */ virtual int trackWidth(int categoryNum) const override { CategoryID id(Track, categoryNum); return m_rows[findIdentifier(id)].options.trackWidth(); } /** * @param index, the 0-based index for the folder boundary. * @return true if there should be a folder separator between category * index and index + 1, and false otherwise. Note that for purposes * of this function, only categories that are required or non-empty * should count. */ virtual bool hasFolderSeparator(int index) const override; /** * @param category The category to get the status of. * @return true if \p category is disabled by the user, and false otherwise. */ virtual bool isDisabled(const CategoryID &category) const override { return m_rows[findIdentifier(category)].options.disabled(); } /** * This moves the widget \p l in the direction given by \p direction, taking * care to make sure that the checkboxes are not moved, and that they are * enabled or disabled as appropriate for the new layout, and that the up and * down buttons are also adjusted as necessary. * * @param id the identifier of the row to move * @param direction the direction to move */ void moveItem(int id, MovementDirection direction); /** * This function actually performs the work of showing the options dialog for * \p category. * * @param category the category to show the options dialog for. */ void showCategoryOptions(TagType category); /** * This function enables or disables the widget in the row identified by \p id, * controlled by \p enable. This function also makes sure that checkboxes are * enabled or disabled as appropriate if they no longer make sense due to the * adjacent category being enabled or disabled. * * @param id the identifier of the row to change. This is *not* the category to * change. * @param enable enables the category if true, disables if false. */ void setCategoryEnabled(int id, bool enable); - /** - * This function enables all of the up buttons. - */ - void enableAllUpButtons(); - - /** - * This function enables all of the down buttons. - */ - void enableAllDownButtons(); - /** * This function returns the identifier of the row at \p position. * * @param position The position to find the identifier of. * @return The unique id of the row at \p position. */ int idOfPosition(int position) const; /** * This function returns the identifier of the row in the m_rows index that * contains \p category and matches \p categoryNum. * * @param category the category to find. * @return the identifier of the category, or MAX_CATEGORIES if it couldn't * be found. */ int findIdentifier(const CategoryID &category) const; private slots: /** * This function reads the tags from \p file and ensures that the dialog will * use those tags until a different file is selected or dataSelected() is * called. * * @param file the path to the local file to read. */ virtual void fileSelected(const QString &file); /** * This function reads the tags from the user-supplied examples and ensures * that the dialog will use those tags until a file is selected using * fileSelected(). */ virtual void dataSelected(); /** * This function brings up a dialog that allows the user to edit the options * for \p id. * * @param id the unique id to bring up the options for. */ virtual void showCategoryOption(int id); /** * This function removes the row identified by id and updates the internal data to be * consistent again, by forwarding the call to removeRow(). * * @param id The unique id to update */ virtual void slotRemoveRow(int id); /** * This function moves \p category up in the layout. * * @param id the unique id of the widget to move up. */ virtual void moveItemUp(int id); /** * This function moves \p category down in the layout. * * @param id the unique id of the widget to move down. */ virtual void moveItemDown(int id); /** * This slot should be called whenever the example input dialog is shown. */ virtual void exampleDialogShown(); /** * This slot should be called whenever the example input dialog is hidden. */ virtual void exampleDialogHidden(); private: /// This is the frame that holds all of the category widgets and checkboxes. QFrame *m_mainFrame; Ui::FileRenamerBase *m_ui; /** * This is the meat of the widget, it holds the rows for the user configuration. It is * initially created such that m_rows[0] is the top and row + 1 is the row just below. * However, this is NOT NECESSARILY true, so don't rely on this. As soon as the user * clicks an arrow to move a row then the order will be messed up. Use row.position to * determine where the row is in the GUI. * * @see idOfPosition * @see findIdentifier */ Rows m_rows; /** * This holds an array of checkboxes that allow the user to insert folder * separators in between categories. */ - DirSeparatorCheckBoxes m_folderSwitches; + QVector m_folderSwitches; ExampleOptionsDialog *m_exampleDialog; /// This is true if we're reading example tags from m_exampleFile. bool m_exampleFromFile; QString m_exampleFile; }; /** * This class contains the backend code to actually implement the file renaming. It performs * the function of moving the files from one location to another, constructing the file name * based off of the user's options (see ConfigCategoryReader) and of setting folder icons * if appropriate. * * @author Michael Pyne */ -class FileRenamer +class FileRenamer final { public: FileRenamer(); /** * Renames the filename on disk of the file represented by item according * to the user configuration stored in KConfig. * * @param item The item to rename. */ void rename(PlaylistItem *item); /** * Renames the filenames on disk of the files given in items according to * the user configuration stored in KConfig. * * @param items The items to rename. */ void rename(const PlaylistItemList &items); /** * Returns the file name that would be generated based on the options read from * interface, which must implement CategoryReaderInterface. (A whole interface is used * so that we can re-use the code to generate filenames from a in-memory GUI and from * KConfig). * * @param interface object to read options/data from. */ static QString fileName(const CategoryReaderInterface &interface); private: /** * Sets the folder icon for elements of the destination path for item (if * there is not already a folder icon set, and if the folder's name has * the album name. */ void setFolderIcon(const QUrl &dst, const PlaylistItem *item); /** * Attempts to rename the file from \a src to \a dest. Returns true if the * operation succeeded. */ bool moveFile(const QString &src, const QString &dest); }; #endif /* JUK_FILERENAMER_H */ // vim: set et sw=4 tw=0 sta: