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: