File indexing completed on 2023-05-30 11:30:46

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