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: