File indexing completed on 2021-12-21 13:27:53

0001 /**
0002  * Copyright (C) 2004, 2007, 2009 Michael Pyne <mpyne@kde.org>
0003  * Copyright (C) 2003 Frerich Raabe <raabe@kde.org>
0004  * Copyright (C) 2014 Arnold Dumas <contact@arnolddumas.fr>
0005  *
0006  * This program is free software; you can redistribute it and/or modify it under
0007  * the terms of the GNU General Public License as published by the Free Software
0008  * Foundation; either version 2 of the License, or (at your option) any later
0009  * version.
0010  *
0011  * This program is distributed in the hope that it will be useful, but WITHOUT ANY
0012  * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
0013  * PARTICULAR PURPOSE. See the GNU General Public License for more details.
0014  *
0015  * You should have received a copy of the GNU General Public License along with
0016  * this program.  If not, see <http://www.gnu.org/licenses/>.
0017  */
0018 
0019 #include "filerenamer.h"
0020 
0021 #include <algorithm>
0022 
0023 #include <KUrlRequester>
0024 #include <kiconloader.h>
0025 #include <KLocalizedString>
0026 #include <kio/job.h>
0027 #include <kdesktopfile.h>
0028 #include <kconfiggroup.h>
0029 #include <KSharedConfig>
0030 #include <klineedit.h>
0031 #include <kmessagebox.h>
0032 
0033 #include <QFile>
0034 #include <QTimer>
0035 #include <QCheckBox>
0036 #include <QDir>
0037 #include <QDialog>
0038 #include <QDialogButtonBox>
0039 #include <QUrl>
0040 #include <QLabel>
0041 #include <QPixmap>
0042 #include <QFrame>
0043 #include <QTreeWidget>
0044 #include <QScrollBar>
0045 #include <QPushButton>
0046 #include <QHeaderView>
0047 
0048 #include "coverinfo.h"
0049 #include "exampleoptions.h"
0050 #include "filehandle.h"
0051 #include "filerenameroptions.h"
0052 #include "iconsupport.h"
0053 #include "juk_debug.h"
0054 #include "juktag.h"
0055 #include "playlist.h" // processEvents()
0056 #include "playlistitem.h"
0057 
0058 class ConfirmationDialog : public QDialog
0059 {
0060 public:
0061     ConfirmationDialog(const QMap<QString, QString> &files,
0062                        QWidget *parent = nullptr)
0063         : QDialog(parent)
0064     {
0065         using namespace IconSupport; // ""_icon
0066 
0067         setModal(true);
0068         setWindowTitle(i18nc("warning about mass file rename", "Warning"));
0069 
0070         auto vboxLayout = new QVBoxLayout(this);
0071         auto hbox = new QWidget(this);
0072         auto hboxVLayout = new QVBoxLayout(hbox);
0073         vboxLayout->addWidget(hbox);
0074 
0075         QLabel *l = new QLabel(hbox);
0076         l->setPixmap(("dialog-warning"_icon).pixmap(KIconLoader::SizeLarge));
0077         hboxVLayout->addWidget(l);
0078 
0079         l = new QLabel(i18n("You are about to rename the following files. "
0080                             "Are you sure you want to continue?"), hbox);
0081         hboxVLayout->addWidget(l);
0082 
0083         QTreeWidget *lv = new QTreeWidget(this);
0084 
0085         QStringList headers;
0086         headers << i18n("Original Name");
0087         headers << i18n("New Name");
0088 
0089         lv->setHeaderLabels(headers);
0090         lv->setRootIsDecorated(false);
0091         vboxLayout->addWidget(lv, 1);
0092 
0093         auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Yes | QDialogButtonBox::Cancel, this);
0094         vboxLayout->addWidget(buttonBox);
0095         connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
0096         connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
0097 
0098         int lvHeight = 0;
0099 
0100         QMap<QString, QString>::ConstIterator it = files.constBegin();
0101         for(; it != files.constEnd(); ++it) {
0102             QTreeWidgetItem *item = new QTreeWidgetItem(lv);
0103             item->setText(0, it.key());
0104 
0105             if (it.key() != it.value()) {
0106                 item->setText(1, it.value());
0107             }
0108 
0109             else {
0110                 item->setText(1, i18n("No Change"));
0111             }
0112 
0113             lvHeight += lv->visualItemRect(item).height();
0114         }
0115 
0116         lvHeight += lv->horizontalScrollBar()->height() + lv->header()->height();
0117         lv->setMinimumHeight(qMin(lvHeight, 400));
0118 
0119         resize(qMin(width(), 500), qMin(minimumHeight(), 400));
0120 
0121         show();
0122     }
0123 };
0124 
0125 //
0126 // Implementation of ConfigCategoryReader
0127 //
0128 
0129 ConfigCategoryReader::ConfigCategoryReader() : CategoryReaderInterface(),
0130     m_currentItem(0)
0131 {
0132     KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer");
0133 
0134     QList<int> categoryOrder = config.readEntry("CategoryOrder", QList<int>());
0135     int categoryCount[NumTypes] = { 0 }; // Keep track of each category encountered.
0136 
0137     // Set a default:
0138 
0139     if(categoryOrder.isEmpty())
0140         categoryOrder << Artist << Album << Title << Track;
0141 
0142     QList<int>::ConstIterator catIt = categoryOrder.constBegin();
0143     for(; catIt != categoryOrder.constEnd(); ++catIt)
0144     {
0145         int catCount = categoryCount[*catIt]++;
0146         TagType category = static_cast<TagType>(*catIt);
0147         CategoryID catId(category, catCount);
0148 
0149         m_options[catId] = TagRenamerOptions(catId);
0150         m_categoryOrder << catId;
0151     }
0152 
0153     m_folderSeparators.fill(false, m_categoryOrder.count() - 1);
0154 
0155     QList<int> checkedSeparators = config.readEntry("CheckedDirSeparators", QList<int>());
0156 
0157     QList<int>::ConstIterator it = checkedSeparators.constBegin();
0158     for(; it != checkedSeparators.constEnd(); ++it) {
0159         if(*it < m_folderSeparators.count())
0160             m_folderSeparators[*it] = true;
0161     }
0162 
0163     m_musicFolder = config.readPathEntry("MusicFolder", "${HOME}/music");
0164     m_separator = config.readEntry("Separator", " - ");
0165 }
0166 
0167 QString ConfigCategoryReader::categoryValue(TagType type) const
0168 {
0169     if(!m_currentItem)
0170         return QString();
0171 
0172     Tag *tag = m_currentItem->file().tag();
0173 
0174     switch(type) {
0175     case Track:
0176         return QString::number(tag->track());
0177 
0178     case Year:
0179         return QString::number(tag->year());
0180 
0181     case Title:
0182         return tag->title();
0183 
0184     case Artist:
0185         return tag->artist();
0186 
0187     case Album:
0188         return tag->album();
0189 
0190     case Genre:
0191         return tag->genre();
0192 
0193     default:
0194         return QString();
0195     }
0196 }
0197 
0198 QString ConfigCategoryReader::prefix(const CategoryID &category) const
0199 {
0200     return m_options[category].prefix();
0201 }
0202 
0203 QString ConfigCategoryReader::suffix(const CategoryID &category) const
0204 {
0205     return m_options[category].suffix();
0206 }
0207 
0208 TagRenamerOptions::EmptyActions ConfigCategoryReader::emptyAction(const CategoryID &category) const
0209 {
0210     return m_options[category].emptyAction();
0211 }
0212 
0213 QString ConfigCategoryReader::emptyText(const CategoryID &category) const
0214 {
0215     return m_options[category].emptyText();
0216 }
0217 
0218 QList<CategoryID> ConfigCategoryReader::categoryOrder() const
0219 {
0220     return m_categoryOrder;
0221 }
0222 
0223 QString ConfigCategoryReader::separator() const
0224 {
0225     return m_separator;
0226 }
0227 
0228 QString ConfigCategoryReader::musicFolder() const
0229 {
0230     return m_musicFolder;
0231 }
0232 
0233 int ConfigCategoryReader::trackWidth(int categoryNum) const
0234 {
0235     return m_options[CategoryID(Track, categoryNum)].trackWidth();
0236 }
0237 
0238 bool ConfigCategoryReader::hasFolderSeparator(int index) const
0239 {
0240     if(index >= m_folderSeparators.count())
0241         return false;
0242     return m_folderSeparators[index];
0243 }
0244 
0245 bool ConfigCategoryReader::isDisabled(const CategoryID &category) const
0246 {
0247     return m_options[category].disabled();
0248 }
0249 
0250 //
0251 // Implementation of FileRenamerWidget
0252 //
0253 
0254 FileRenamerWidget::FileRenamerWidget(QWidget *parent) :
0255     QWidget(parent),
0256     CategoryReaderInterface(),
0257     m_ui(new Ui::FileRenamerBase),
0258     m_exampleFromFile(false)
0259 {
0260     m_ui->setupUi(this);
0261 
0262     // This must be created before createTagRows() is called.
0263 
0264     m_exampleDialog = new ExampleOptionsDialog(this);
0265 
0266     createTagRows();
0267     loadConfig();
0268 
0269     // Add correct text to combo box.
0270     m_ui->m_category->clear();
0271     for(int i = StartTag; i < NumTypes; ++i) {
0272         QString category = TagRenamerOptions::tagTypeText(static_cast<TagType>(i));
0273         m_ui->m_category->addItem(category);
0274     }
0275 
0276     connect(m_exampleDialog, &ExampleOptionsDialog::signalShown,
0277             this,            &FileRenamerWidget::exampleDialogShown);
0278     connect(m_exampleDialog, &ExampleOptionsDialog::signalHidden,
0279             this,            &FileRenamerWidget::exampleDialogHidden);
0280     connect(m_exampleDialog, &ExampleOptionsDialog::dataChanged,
0281             this,            &FileRenamerWidget::dataSelected);
0282     connect(m_exampleDialog, &ExampleOptionsDialog::fileChanged,
0283             this,            &FileRenamerWidget::fileSelected);
0284     connect(m_ui->dlgButtonBox, &QDialogButtonBox::accepted, this, [this]() {
0285                 emit accepted();
0286             });
0287     connect(m_ui->dlgButtonBox, &QDialogButtonBox::rejected, this, [this]() {
0288                 emit rejected();
0289             });
0290 
0291     exampleTextChanged();
0292 }
0293 
0294 void FileRenamerWidget::loadConfig()
0295 {
0296     QList<int> checkedSeparators;
0297     KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer");
0298 
0299     for(int i = 0; i < m_rows.count(); ++i)
0300         m_rows[i].options = TagRenamerOptions(m_rows[i].category);
0301 
0302     checkedSeparators = config.readEntry("CheckedDirSeparators", QList<int>());
0303 
0304     foreach(int separator, checkedSeparators) {
0305         if(separator < m_folderSwitches.count())
0306             m_folderSwitches[separator]->setChecked(true);
0307     }
0308 
0309     QString path = config.readEntry("MusicFolder", "${HOME}/music");
0310     m_ui->m_musicFolder->setUrl(QUrl::fromLocalFile(path));
0311     m_ui->m_musicFolder->setMode(KFile::Directory |
0312                                  KFile::ExistingOnly |
0313                                  KFile::LocalOnly);
0314 
0315     m_ui->m_separator->setEditText(config.readEntry("Separator", " - "));
0316 }
0317 
0318 void FileRenamerWidget::saveConfig()
0319 {
0320     KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer");
0321     QList<int> checkedSeparators;
0322     QList<int> categoryOrder;
0323 
0324     for(int i = 0; i < m_rows.count(); ++i) {
0325         int rowId = idOfPosition(i); // Write out in GUI order, not m_rows order
0326         m_rows[rowId].options.saveConfig(m_rows[rowId].category.categoryNumber);
0327         categoryOrder += m_rows[rowId].category.category;
0328     }
0329 
0330     for(int i = 0; i < m_folderSwitches.count(); ++i)
0331         if(m_folderSwitches[i]->isChecked() == true)
0332             checkedSeparators += i;
0333 
0334     config.writeEntry("CheckedDirSeparators", checkedSeparators);
0335     config.writeEntry("CategoryOrder", categoryOrder);
0336     config.writePathEntry("MusicFolder", m_ui->m_musicFolder->url().path());
0337     config.writeEntry("Separator", m_ui->m_separator->currentText());
0338 
0339     config.sync();
0340 }
0341 
0342 FileRenamerWidget::~FileRenamerWidget()
0343 {
0344 }
0345 
0346 int FileRenamerWidget::addRowCategory(TagType category)
0347 {
0348     using namespace IconSupport; // ""_icon
0349 
0350     static QIcon up   = "go-up"_icon;
0351     static QIcon down = "go-down"_icon;
0352 
0353     // Find number of categories already of this type.
0354     int categoryCount = std::count_if(m_rows.cbegin(), m_rows.cend(),
0355             [category](const Row &r) { return r.category.category == category; }
0356             );
0357 
0358     Row row;
0359 
0360     row.category = CategoryID(category, categoryCount);
0361     row.position = m_rows.count();
0362 
0363     QFrame *frame = new QFrame(m_mainFrame);
0364     QHBoxLayout *frameLayout = new QHBoxLayout(frame);
0365     frameLayout->setContentsMargins(3, 3, 3, 3);
0366 
0367     row.widget = frame;
0368     frame->setFrameShape(QFrame::Box);
0369     frame->setLineWidth(1);
0370 
0371     QBoxLayout *mainFrameLayout = static_cast<QBoxLayout *>(m_mainFrame->layout());
0372     mainFrameLayout->addWidget(frame, 1);
0373 
0374     QFrame *buttons = new QFrame(frame);
0375     QVBoxLayout *buttonLayout = new QVBoxLayout(buttons);
0376     frameLayout->addWidget(buttons);
0377     buttons->setFrameStyle(QFrame::Plain | QFrame::Box);
0378     buttons->setLineWidth(1);
0379 
0380     row.upButton = new QPushButton(buttons);
0381     row.downButton = new QPushButton(buttons);
0382 
0383     row.upButton->setIcon(up);
0384     row.downButton->setIcon(down);
0385     row.upButton->setFlat(true);
0386     row.downButton->setFlat(true);
0387 
0388     buttonLayout->addWidget(row.upButton);
0389     buttonLayout->addWidget(row.downButton);
0390 
0391     QString labelText = QString("<b>%1</b>").arg(TagRenamerOptions::tagTypeText(category));
0392     QLabel *label = new QLabel(labelText, frame);
0393     frameLayout->addWidget(label, 1);
0394     label->setAlignment(Qt::AlignCenter);
0395 
0396     QVBoxLayout *optionLayout = new QVBoxLayout;
0397     frameLayout->addLayout(optionLayout);
0398 
0399     row.enableButton = new QPushButton(i18nc("remove music genre from file renamer", "Remove"), frame);
0400     optionLayout->addWidget(row.enableButton);
0401 
0402     row.optionsButton = new QPushButton(i18nc("file renamer genre options", "Options"), frame);
0403     optionLayout->addWidget(row.optionsButton);
0404 
0405     row.widget->show();
0406     m_rows.append(row);
0407 
0408     assignPositionHandlerForRow(row);
0409 
0410     // Disable add button if there's too many rows.
0411     if(m_rows.count() == MAX_CATEGORIES)
0412         m_ui->m_insertCategory->setEnabled(false);
0413 
0414     return row.position;
0415 }
0416 
0417 void FileRenamerWidget::assignPositionHandlerForRow(Row &row)
0418 {
0419     const auto id = row.position;
0420 
0421     disconnect(row.upButton);
0422     disconnect(row.downButton);
0423     disconnect(row.enableButton);
0424     disconnect(row.optionsButton);
0425 
0426     connect(row.upButton, &QPushButton::clicked, this, [this, id]() {
0427             this->moveItemUp(id);
0428         });
0429     connect(row.downButton, &QPushButton::clicked, this, [this, id]() {
0430             this->moveItemDown(id);
0431         });
0432     connect(row.enableButton, &QPushButton::clicked, this, [this, id]() {
0433             this->slotRemoveRow(id);
0434         });
0435     connect(row.optionsButton, &QPushButton::clicked, this, [this, id]() {
0436             this->showCategoryOption(id);
0437         });
0438 }
0439 
0440 bool FileRenamerWidget::removeRow(int id)
0441 {
0442     if(id >= m_rows.count()) {
0443         qCWarning(JUK_LOG) << "Trying to remove row, but " << id << " is out-of-range.\n";
0444         return false;
0445     }
0446 
0447     if(m_rows.count() == 1) {
0448         qCCritical(JUK_LOG) << "Can't remove last row of File Renamer.\n";
0449         return false;
0450     }
0451 
0452     delete m_rows[id].widget;
0453     m_rows[id].widget        = nullptr;
0454     m_rows[id].enableButton  = nullptr;
0455     m_rows[id].upButton      = nullptr;
0456     m_rows[id].optionsButton = nullptr;
0457     m_rows[id].downButton    = nullptr;
0458 
0459     int checkboxPosition = 0; // Remove first checkbox.
0460 
0461     // If not the first row, remove the checkbox before it.
0462     if(m_rows[id].position > 0)
0463         checkboxPosition = m_rows[id].position - 1;
0464 
0465     // The checkbox is contained within a layout widget, so the layout
0466     // widget is the one the needs to die.
0467     delete m_folderSwitches[checkboxPosition]->parent();
0468     m_folderSwitches.erase(&m_folderSwitches[checkboxPosition]);
0469 
0470     // Go through all the rows and if they have the same category and a
0471     // higher categoryNumber, decrement the number.  Also update the
0472     // position identifier.
0473     for(int i = 0; i < m_rows.count(); ++i) {
0474         if(i == id)
0475             continue; // Don't mess with ourself.
0476 
0477         if((m_rows[id].category.category == m_rows[i].category.category) &&
0478            (m_rows[id].category.categoryNumber < m_rows[i].category.categoryNumber))
0479         {
0480             --m_rows[i].category.categoryNumber;
0481         }
0482 
0483         // Items are moving up.
0484         if(m_rows[id].position < m_rows[i].position)
0485             --m_rows[i].position;
0486     }
0487 
0488     // Every row after the one we delete will have a different identifier, since
0489     // the identifier is simply its index into m_rows.  So we need to re-do the
0490     // signal mappings for the affected rows after updating its position.
0491     for(int i = id + 1; i < m_rows.count(); ++i)
0492         assignPositionHandlerForRow(m_rows[i]);
0493 
0494     m_rows.erase(&m_rows[id]);
0495 
0496     // Make sure we update the buttons of affected rows.
0497     m_rows[idOfPosition(0)].upButton->setEnabled(false);
0498     m_rows[idOfPosition(m_rows.count() - 1)].downButton->setEnabled(false);
0499 
0500     // We can insert another row now, make sure GUI is updated to match.
0501     m_ui->m_insertCategory->setEnabled(true);
0502 
0503     QTimer::singleShot(0, this, &FileRenamerWidget::exampleTextChanged);
0504     return true;
0505 }
0506 
0507 void FileRenamerWidget::addFolderSeparatorCheckbox()
0508 {
0509     QWidget *temp = new QWidget(m_mainFrame);
0510     m_mainFrame->layout()->addWidget(temp);
0511 
0512     QHBoxLayout *l = new QHBoxLayout(temp);
0513 
0514     QCheckBox *cb = new QCheckBox(i18n("Insert folder separator"), temp);
0515     m_folderSwitches.append(cb);
0516     l->addWidget(cb, 0, Qt::AlignCenter);
0517     cb->setChecked(false);
0518 
0519     connect(cb, &QCheckBox::toggled, this, &FileRenamerWidget::exampleTextChanged);
0520 
0521     temp->show();
0522 }
0523 
0524 void FileRenamerWidget::createTagRows()
0525 {
0526     KConfigGroup config(KSharedConfig::openConfig(), "FileRenamer");
0527     QList<int> categoryOrder = config.readEntry("CategoryOrder", QList<int>());
0528 
0529     if(categoryOrder.isEmpty())
0530         categoryOrder << Artist << Album << Title << Track;
0531 
0532     // Setup arrays.
0533     m_rows.reserve(categoryOrder.count());
0534     m_folderSwitches.reserve(categoryOrder.count() - 1);
0535 
0536     m_mainFrame = new QFrame(m_ui->m_mainView);
0537     m_ui->m_mainView->setWidget(m_mainFrame);
0538     m_ui->m_mainView->setWidgetResizable(true);
0539 
0540     QVBoxLayout *frameLayout = new QVBoxLayout(m_mainFrame);
0541     frameLayout->setContentsMargins(10, 10, 10, 10);
0542     frameLayout->setSpacing(5);
0543 
0544     // OK, the deal with the categoryOrder variable is that we need to create
0545     // the rows in the order that they were saved in (the order given by categoryOrder).
0546     // The signal mappers operate according to the row identifier.  To find the position of
0547     // a row given the identifier, use m_rows[id].position.  To find the id of a given
0548     // position, use idOfPosition(position).
0549 
0550     for(auto it = categoryOrder.cbegin(); it != categoryOrder.cend(); ++it) {
0551         if(*it < StartTag || *it >= NumTypes) {
0552             qCCritical(JUK_LOG) << "Invalid category encountered in file renamer configuration.\n";
0553             continue;
0554         }
0555 
0556         if(m_rows.count() == MAX_CATEGORIES) {
0557             qCCritical(JUK_LOG) << "Maximum number of File Renamer tags reached, bailing.\n";
0558             break;
0559         }
0560 
0561         addRowCategory(static_cast<TagType>(*it));
0562 
0563         // Insert the directory separator checkbox if this isn't the last
0564         // item.
0565 
0566         if((it + 1) != categoryOrder.constEnd())
0567             addFolderSeparatorCheckbox();
0568     }
0569 
0570     m_rows.first().upButton->setEnabled(false);
0571     m_rows.last().downButton->setEnabled(false);
0572 
0573     // If we have maximum number of categories already, don't let the user
0574     // add more.
0575     if(m_rows.count() >= MAX_CATEGORIES)
0576         m_ui->m_insertCategory->setEnabled(false);
0577 }
0578 
0579 void FileRenamerWidget::exampleTextChanged()
0580 {
0581     // Just use .mp3 as an example
0582     if(m_exampleFromFile && (m_exampleFile.isEmpty() ||
0583                              !FileHandle(m_exampleFile).tag()->isValid()))
0584     {
0585         m_ui->m_exampleText->setText(i18n("No file selected, or selected file has no tags."));
0586         return;
0587     }
0588 
0589     m_ui->m_exampleText->setText(FileRenamer::fileName(*this) + ".mp3");
0590 }
0591 
0592 QString FileRenamerWidget::fileCategoryValue(TagType category) const
0593 {
0594     FileHandle file(m_exampleFile);
0595     Tag *tag = file.tag();
0596 
0597     switch(category) {
0598     case Track:
0599         return QString::number(tag->track());
0600 
0601     case Year:
0602         return QString::number(tag->year());
0603 
0604     case Title:
0605         return tag->title();
0606 
0607     case Artist:
0608         return tag->artist();
0609 
0610     case Album:
0611         return tag->album();
0612 
0613     case Genre:
0614         return tag->genre();
0615 
0616     default:
0617         return QString();
0618     }
0619 }
0620 
0621 QString FileRenamerWidget::categoryValue(TagType category) const
0622 {
0623     if(m_exampleFromFile)
0624         return fileCategoryValue(category);
0625 
0626     const ExampleOptions *example = m_exampleDialog->widget();
0627 
0628     switch (category) {
0629     case Track:
0630         return example->m_exampleTrack->text();
0631 
0632     case Year:
0633         return example->m_exampleYear->text();
0634 
0635     case Title:
0636         return example->m_exampleTitle->text();
0637 
0638     case Artist:
0639         return example->m_exampleArtist->text();
0640 
0641     case Album:
0642         return example->m_exampleAlbum->text();
0643 
0644     case Genre:
0645         return example->m_exampleGenre->text();
0646 
0647     default:
0648         return QString();
0649     }
0650 }
0651 
0652 QList<CategoryID> FileRenamerWidget::categoryOrder() const
0653 {
0654     QList<CategoryID> list;
0655 
0656     // Iterate in GUI row order.
0657     for(int i = 0; i < m_rows.count(); ++i) {
0658         int rowId = idOfPosition(i);
0659         list += m_rows[rowId].category;
0660     }
0661 
0662     return list;
0663 }
0664 
0665 bool FileRenamerWidget::hasFolderSeparator(int index) const
0666 {
0667     if(index >= m_folderSwitches.count())
0668         return false;
0669     return m_folderSwitches[index]->isChecked();
0670 }
0671 
0672 void FileRenamerWidget::moveItem(int id, MovementDirection direction)
0673 {
0674     QWidget *l = m_rows[id].widget;
0675     int bottom = m_rows.count() - 1;
0676     int pos = m_rows[id].position;
0677     int newPos = (direction == MoveUp) ? pos - 1 : pos + 1;
0678 
0679     // Item we're moving can't go further down after this.
0680 
0681     if((pos == (bottom - 1) && direction == MoveDown) ||
0682        (pos == bottom && direction == MoveUp))
0683     {
0684         int idBottomRow = idOfPosition(bottom);
0685         int idAboveBottomRow = idOfPosition(bottom - 1);
0686 
0687         m_rows[idBottomRow].downButton->setEnabled(true);
0688         m_rows[idAboveBottomRow].downButton->setEnabled(false);
0689     }
0690 
0691     // We're moving the top item, do some button switching.
0692 
0693     if((pos == 0 && direction == MoveDown) || (pos == 1 && direction == MoveUp)) {
0694         int idTopItem = idOfPosition(0);
0695         int idBelowTopItem = idOfPosition(1);
0696 
0697         m_rows[idTopItem].upButton->setEnabled(true);
0698         m_rows[idBelowTopItem].upButton->setEnabled(false);
0699     }
0700 
0701     // This is the item we're swapping with.
0702 
0703     int idSwitchWith = idOfPosition(newPos);
0704     QWidget *w = m_rows[idSwitchWith].widget;
0705 
0706     // Update the table of widget rows.
0707 
0708     std::swap(m_rows[id].position, m_rows[idSwitchWith].position);
0709 
0710     // Move the item two spaces above/below its previous position.  It has to
0711     // be 2 spaces because of the checkbox.
0712 
0713     QBoxLayout *layout = dynamic_cast<QBoxLayout *>(m_mainFrame->layout());
0714     if ( !layout )
0715         return;
0716 
0717     layout->removeWidget(l);
0718     layout->insertWidget(2 * newPos, l);
0719 
0720     // Move the top item two spaces in the opposite direction, for a similar
0721     // reason.
0722 
0723     layout->removeWidget(w);
0724     layout->insertWidget(2 * pos, w);
0725     layout->invalidate();
0726 
0727     QTimer::singleShot(0, this, &FileRenamerWidget::exampleTextChanged);
0728 }
0729 
0730 int FileRenamerWidget::idOfPosition(int position) const
0731 {
0732     if(position >= m_rows.count()) {
0733         qCCritical(JUK_LOG) << "Search for position " << position << " out-of-range.\n";
0734         return -1;
0735     }
0736 
0737     for(int i = 0; i < m_rows.count(); ++i)
0738         if(m_rows[i].position == position)
0739             return i;
0740 
0741     qCCritical(JUK_LOG) << "Unable to find identifier for position " << position;
0742     return -1;
0743 }
0744 
0745 int FileRenamerWidget::findIdentifier(const CategoryID &category) const
0746 {
0747     for(int index = 0; index < m_rows.count(); ++index)
0748         if(m_rows[index].category == category)
0749             return index;
0750 
0751     qCCritical(JUK_LOG) << "Unable to find match for category " <<
0752         TagRenamerOptions::tagTypeText(category.category) <<
0753         ", number " << category.categoryNumber;
0754 
0755     return MAX_CATEGORIES;
0756 }
0757 
0758 void FileRenamerWidget::showCategoryOption(int id)
0759 {
0760     TagOptionsDialog *dialog = new TagOptionsDialog(this, m_rows[id].options, m_rows[id].category.categoryNumber);
0761 
0762     if(dialog->exec() == QDialog::Accepted) {
0763         m_rows[id].options = dialog->options();
0764         exampleTextChanged();
0765     }
0766 
0767     delete dialog;
0768 }
0769 
0770 void FileRenamerWidget::moveItemUp(int id)
0771 {
0772     moveItem(id, MoveUp);
0773 }
0774 
0775 void FileRenamerWidget::moveItemDown(int id)
0776 {
0777     moveItem(id, MoveDown);
0778 }
0779 
0780 void FileRenamerWidget::toggleExampleDialog()
0781 {
0782     m_exampleDialog->setHidden(!m_exampleDialog->isHidden());
0783 }
0784 
0785 void FileRenamerWidget::insertCategory()
0786 {
0787     TagType category = static_cast<TagType>(m_ui->m_category->currentIndex());
0788     if(m_ui->m_category->currentIndex() < 0 || category >= NumTypes) {
0789         qCCritical(JUK_LOG) << "Trying to add unknown category somehow.\n";
0790         return;
0791     }
0792 
0793     // We need to enable the down button of the current bottom row since it
0794     // can now move down.
0795     int idBottom = idOfPosition(m_rows.count() - 1);
0796     m_rows[idBottom].downButton->setEnabled(true);
0797 
0798     addFolderSeparatorCheckbox();
0799 
0800     // Identifier of new row.
0801     int id = addRowCategory(category);
0802 
0803     // Set its down button to be disabled.
0804     m_rows[id].downButton->setEnabled(false);
0805 
0806     m_mainFrame->layout()->invalidate();
0807     m_ui->m_mainView->update();
0808 
0809     // Now update according to the code in loadConfig().
0810     m_rows[id].options = TagRenamerOptions(m_rows[id].category);
0811     exampleTextChanged();
0812 }
0813 
0814 void FileRenamerWidget::exampleDialogShown()
0815 {
0816     m_ui->m_showExample->setText(i18n("Hide Renamer Test Dialog"));
0817 }
0818 
0819 void FileRenamerWidget::exampleDialogHidden()
0820 {
0821     m_ui->m_showExample->setText(i18n("Show Renamer Test Dialog"));
0822 }
0823 
0824 void FileRenamerWidget::fileSelected(const QString &file)
0825 {
0826     m_exampleFromFile = true;
0827     m_exampleFile = file;
0828     exampleTextChanged();
0829 }
0830 
0831 void FileRenamerWidget::dataSelected()
0832 {
0833     m_exampleFromFile = false;
0834     exampleTextChanged();
0835 }
0836 
0837 QString FileRenamerWidget::separator() const
0838 {
0839     return m_ui->m_separator->currentText();
0840 }
0841 
0842 QString FileRenamerWidget::musicFolder() const
0843 {
0844     return m_ui->m_musicFolder->url().path();
0845 }
0846 
0847 void FileRenamerWidget::slotRemoveRow(int id)
0848 {
0849     // Remove the given identified row.
0850     if(!removeRow(id))
0851         qCCritical(JUK_LOG) << "Unable to remove row " << id;
0852 }
0853 
0854 //
0855 // Implementation of FileRenamer
0856 //
0857 
0858 FileRenamer::FileRenamer()
0859 {
0860 }
0861 
0862 void FileRenamer::rename(PlaylistItem *item)
0863 {
0864     PlaylistItemList list;
0865     list.append(item);
0866 
0867     rename(list);
0868 }
0869 
0870 void FileRenamer::rename(const PlaylistItemList &items)
0871 {
0872     ConfigCategoryReader reader;
0873     QStringList errorFiles;
0874     QMap<QString, QString> map;
0875     QMap<QString, PlaylistItem *> itemMap;
0876 
0877     for(PlaylistItemList::ConstIterator it = items.constBegin(); it != items.constEnd(); ++it) {
0878         reader.setPlaylistItem(*it);
0879         QString oldFile = (*it)->file().absFilePath();
0880         QString extension = (*it)->file().fileInfo().suffix();
0881         QString newFile = fileName(reader) + '.' + extension;
0882 
0883         if(oldFile != newFile) {
0884             map[oldFile] = newFile;
0885             itemMap[oldFile] = *it;
0886         }
0887     }
0888 
0889     if(itemMap.isEmpty() || ConfirmationDialog(map).exec() != QDialog::Accepted)
0890         return;
0891 
0892     QApplication::setOverrideCursor(Qt::WaitCursor);
0893     for(QMap<QString, QString>::ConstIterator it = map.constBegin();
0894         it != map.constEnd(); ++it)
0895     {
0896         if(moveFile(it.key(), it.value())) {
0897             itemMap[it.key()]->setFile(it.value());
0898             itemMap[it.key()]->refresh();
0899 
0900             setFolderIcon(QUrl::fromLocalFile(it.value()), itemMap[it.key()]);
0901         }
0902         else
0903             errorFiles << i18n("%1 to %2", it.key(), it.value());
0904 
0905         processEvents();
0906     }
0907     QApplication::restoreOverrideCursor();
0908 
0909     if(!errorFiles.isEmpty())
0910         KMessageBox::errorList(0, i18n("The following rename operations failed:\n"), errorFiles);
0911 }
0912 
0913 bool FileRenamer::moveFile(const QString &src, const QString &dest)
0914 {
0915     qCDebug(JUK_LOG) << "Moving file " << src << " to " << dest;
0916 
0917     QUrl srcURL = QUrl::fromLocalFile(src);
0918     QUrl dstURL = QUrl::fromLocalFile(dest);
0919 
0920     if(!srcURL.isValid() || !dstURL.isValid() || srcURL == dstURL)
0921         return false;
0922 
0923     QUrl dir = dstURL.adjusted(QUrl::RemoveFilename); // resolves to path w/out filename
0924     if(!QDir().mkpath(dir.path())) {
0925         qCCritical(JUK_LOG) << "Unable to create directory " << dir.path();
0926         return false;
0927     }
0928 
0929     // Move the file.
0930     KIO::Job *job = KIO::file_move(srcURL, dstURL);
0931     return job->exec();
0932 }
0933 
0934 void FileRenamer::setFolderIcon(const QUrl &dstURL, const PlaylistItem *item)
0935 {
0936     if(item->file().tag()->album().isEmpty() ||
0937        !item->file().coverInfo()->hasCover())
0938     {
0939         return;
0940     }
0941 
0942     // Split path, and go through each path element.  If a path element has
0943     // the album information, set its folder icon.
0944     QStringList elements = dstURL.path().split('/',
0945 #if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
0946             Qt::SkipEmptyParts
0947 #else
0948             QString::SkipEmptyParts
0949 #endif
0950             );
0951     QString path;
0952 
0953     for(QStringList::ConstIterator it = elements.constBegin(); it != elements.constEnd(); ++it) {
0954         path.append('/' + (*it));
0955 
0956         qCDebug(JUK_LOG) << "Checking path: " << path;
0957         if((*it).contains(item->file().tag()->album()) &&
0958            QDir(path).exists() &&
0959            !QFile::exists(path + "/.directory"))
0960         {
0961             // Seems to be a match, let's set the folder icon for the current
0962             // path.  First we should write out the file.
0963 
0964             QPixmap thumb = item->file().coverInfo()->pixmap(CoverInfo::Thumbnail);
0965             thumb.save(path + "/.juk-thumbnail.png", "PNG");
0966 
0967             KDesktopFile dirFile(path + "/.directory");
0968             KConfigGroup desktopGroup(dirFile.desktopGroup());
0969 
0970             if(!desktopGroup.hasKey("Icon")) {
0971                 desktopGroup.writePathEntry("Icon", QString("%1/.juk-thumbnail.png").arg(path));
0972                 dirFile.sync();
0973             }
0974 
0975             return;
0976         }
0977     }
0978 }
0979 
0980 /**
0981  * Returns iterator pointing to the last item enabled in the given list with
0982  * a non-empty value (or is required to be included).
0983  */
0984 QList<CategoryID>::ConstIterator lastEnabledItem(const QList<CategoryID> &list,
0985                                                  const CategoryReaderInterface &interface)
0986 {
0987     QList<CategoryID>::ConstIterator it = list.constBegin();
0988     QList<CategoryID>::ConstIterator last = list.constEnd();
0989 
0990     for(; it != list.constEnd(); ++it) {
0991         if(interface.isRequired(*it) || (!interface.isDisabled(*it) &&
0992               !interface.categoryValue((*it).category).isEmpty()))
0993         {
0994             last = it;
0995         }
0996     }
0997 
0998     return last;
0999 }
1000 
1001 QString FileRenamer::fileName(const CategoryReaderInterface &interface)
1002 {
1003     const QList<CategoryID> categoryOrder = interface.categoryOrder();
1004     const QString separator = interface.separator();
1005     const QString folder = interface.musicFolder();
1006     QList<CategoryID>::ConstIterator lastEnabled;
1007     int i = 0;
1008     QStringList list;
1009     QChar dirSeparator (QDir::separator());
1010 
1011     // Use lastEnabled to properly handle folder separators.
1012     lastEnabled = lastEnabledItem(categoryOrder, interface);
1013     bool pastLast = false; // Toggles to true once we've passed lastEnabled.
1014 
1015     for(QList<CategoryID>::ConstIterator it = categoryOrder.constBegin();
1016             it != categoryOrder.constEnd();
1017             ++it, ++i)
1018     {
1019         if(it == lastEnabled)
1020             pastLast = true;
1021 
1022         if(interface.isDisabled(*it))
1023             continue;
1024 
1025         QString value = interface.value(*it);
1026 
1027         // The user can use the folder separator checkbox to add folders, so don't allow
1028         // slashes that slip in to accidentally create new folders.  Should we filter this
1029         // back out when showing it in the GUI?
1030         value.replace('/', "%2f");
1031 
1032         if(!pastLast && interface.hasFolderSeparator(i))
1033             value.append(dirSeparator);
1034 
1035         if(interface.isRequired(*it) || !value.isEmpty())
1036             list.append(value);
1037     }
1038 
1039     // Construct a single string representation, handling strings ending in
1040     // '/' specially
1041 
1042     QString result;
1043 
1044     for(QStringList::ConstIterator it = list.constBegin(); it != list.constEnd(); /* Empty */) {
1045         result += *it;
1046 
1047         ++it; // Manually advance iterator to check for end-of-list.
1048 
1049         // Add separator unless at a directory boundary
1050         if(it != list.constEnd() &&
1051            !(*it).startsWith(dirSeparator) && // Check beginning of next item.
1052            !result.endsWith(dirSeparator))
1053         {
1054             result += separator;
1055         }
1056     }
1057 
1058     return QString(folder + dirSeparator + result);
1059 }
1060 
1061 // vim: set et sw=4 tw=0 sta: