File indexing completed on 2023-05-30 11:30:52
0001 /** 0002 * Copyright (C) 2002-2004 Scott Wheeler <wheeler@kde.org> 0003 * 0004 * This program is free software; you can redistribute it and/or modify it under 0005 * the terms of the GNU General Public License as published by the Free Software 0006 * Foundation; either version 2 of the License, or (at your option) any later 0007 * version. 0008 * 0009 * This program is distributed in the hope that it will be useful, but WITHOUT ANY 0010 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 0011 * PARTICULAR PURPOSE. See the GNU General Public License for more details. 0012 * 0013 * You should have received a copy of the GNU General Public License along with 0014 * this program. If not, see <http://www.gnu.org/licenses/>. 0015 */ 0016 0017 #include "tageditor.h" 0018 0019 #include <kactioncollection.h> 0020 #include <kcombobox.h> 0021 #include <klineedit.h> 0022 #include <ktextedit.h> 0023 #include <kmessagebox.h> 0024 #include <KSharedConfig> 0025 #include <KConfigGroup> 0026 #include <kiconloader.h> 0027 #include <ktoggleaction.h> 0028 #include <KLocalizedString> 0029 #include <kwidgetsaddons_version.h> 0030 0031 #include <QAction> 0032 #include <QApplication> 0033 #include <QCheckBox> 0034 #include <QDir> 0035 #include <QEventLoop> 0036 #include <QHBoxLayout> 0037 #include <QLabel> 0038 #include <QRegularExpression> 0039 #include <QSizePolicy> 0040 #include <QValidator> 0041 0042 #include <id3v1genres.h> 0043 0044 #include "actioncollection.h" 0045 #include "collectionlist.h" 0046 #include "iconsupport.h" 0047 #include "juk_debug.h" 0048 #include "juktag.h" 0049 #include "playlistitem.h" 0050 #include "tagtransactionmanager.h" 0051 0052 #undef KeyRelease 0053 0054 class FileNameValidator final : public QValidator 0055 { 0056 public: 0057 FileNameValidator(QObject *parent, const char *name = 0) : 0058 QValidator(parent) 0059 { 0060 setObjectName( QLatin1String( name ) ); 0061 } 0062 0063 virtual void fixup(QString &s) const override 0064 { 0065 s.remove('/'); 0066 } 0067 0068 virtual State validate(QString &s, int &) const override 0069 { 0070 if(s.contains('/')) 0071 return Invalid; 0072 return Acceptable; 0073 } 0074 }; 0075 0076 class FixedHLayout final : public QHBoxLayout 0077 { 0078 public: 0079 FixedHLayout(QWidget *parent, int margin = 0, int spacing = -1, const char *name = 0) : 0080 QHBoxLayout(parent), 0081 m_width(-1) 0082 { 0083 setContentsMargins(margin, margin, margin, margin); 0084 setSpacing(spacing); 0085 setObjectName(QLatin1String(name)); 0086 } 0087 FixedHLayout(QLayout *parentLayout, int spacing = -1, const char *name = 0) : 0088 QHBoxLayout(), 0089 m_width(-1) 0090 { 0091 parentLayout->addItem(this); 0092 setSpacing(spacing); 0093 setObjectName(QLatin1String(name)); 0094 } 0095 void setWidth(int w = -1) 0096 { 0097 m_width = w == -1 ? QHBoxLayout::minimumSize().width() : w; 0098 } 0099 virtual QSize minimumSize() const override 0100 { 0101 QSize s = QHBoxLayout::minimumSize(); 0102 s.setWidth(m_width); 0103 return s; 0104 } 0105 private: 0106 int m_width; 0107 }; 0108 0109 class CollectionObserver final 0110 { 0111 public: 0112 CollectionObserver(TagEditor *parent) 0113 { 0114 QObject::connect(&CollectionList::instance()->signaller, &PlaylistInterfaceSignaller::playingItemDataChanged, parent, [parent]{ 0115 if(parent && parent->m_currentPlaylist && parent->isVisible()) 0116 parent->slotSetItems(parent->m_currentPlaylist->selectedItems()); 0117 }); 0118 } 0119 }; 0120 0121 //////////////////////////////////////////////////////////////////////////////// 0122 // public members 0123 //////////////////////////////////////////////////////////////////////////////// 0124 0125 TagEditor::TagEditor(QWidget *parent) : 0126 QWidget(parent), 0127 m_currentPlaylist(0), 0128 m_observer(0), 0129 m_performingSave(false) 0130 { 0131 setupActions(); 0132 setupLayout(); 0133 readConfig(); 0134 m_dataChanged = false; 0135 m_collectionChanged = false; 0136 0137 setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred); 0138 } 0139 0140 TagEditor::~TagEditor() 0141 { 0142 delete m_observer; 0143 saveConfig(); 0144 } 0145 0146 void TagEditor::setupObservers() 0147 { 0148 m_observer = new CollectionObserver(this); 0149 } 0150 0151 //////////////////////////////////////////////////////////////////////////////// 0152 // public slots 0153 //////////////////////////////////////////////////////////////////////////////// 0154 0155 void TagEditor::slotSetItems(const PlaylistItemList &list) 0156 { 0157 if(m_performingSave) 0158 return; 0159 0160 // Store the playlist that we're setting because saveChangesPrompt 0161 // can delete the PlaylistItems in list. 0162 0163 Playlist *itemPlaylist = 0; 0164 if(!list.isEmpty()) 0165 itemPlaylist = list.first()->playlist(); 0166 0167 bool hadPlaylist = m_currentPlaylist != 0; 0168 0169 saveChangesPrompt(); 0170 0171 if(m_currentPlaylist) { 0172 disconnect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem*)), 0173 this, SLOT(slotItemRemoved(PlaylistItem*))); 0174 } 0175 0176 if((hadPlaylist && !m_currentPlaylist) || !itemPlaylist) { 0177 m_currentPlaylist = 0; 0178 m_items.clear(); 0179 } 0180 else { 0181 m_currentPlaylist = itemPlaylist; 0182 0183 // We can't use list here, it may not be valid 0184 0185 m_items = itemPlaylist->selectedItems(); 0186 } 0187 0188 if(m_currentPlaylist) { 0189 connect(m_currentPlaylist, SIGNAL(signalAboutToRemove(PlaylistItem*)), 0190 this, SLOT(slotItemRemoved(PlaylistItem*))); 0191 connect(m_currentPlaylist, SIGNAL(destroyed()), this, SLOT(slotPlaylistRemoved())); 0192 } 0193 0194 if(isVisible()) 0195 slotRefresh(); 0196 else 0197 m_collectionChanged = true; 0198 } 0199 0200 void TagEditor::slotRefresh() 0201 { 0202 // This method takes the list of currently selected m_items and tries to 0203 // figure out how to show that in the tag editor. The current strategy -- 0204 // the most common case -- is to just process the first item. Then we 0205 // check after that to see if there are other m_items and adjust accordingly. 0206 0207 if(m_items.isEmpty() || !m_items.first()->file().tag()) { 0208 slotClear(); 0209 setEnabled(false); 0210 return; 0211 } 0212 0213 setEnabled(true); 0214 0215 PlaylistItem *item = m_items.first(); 0216 0217 Q_ASSERT(item); 0218 0219 Tag *tag = item->file().tag(); 0220 0221 QFileInfo fi(item->file().absFilePath()); 0222 if(!fi.isWritable() && m_items.count() == 1) 0223 setEnabled(false); 0224 0225 artistNameBox->setEditText(tag->artist()); 0226 trackNameBox->setText(tag->title()); 0227 albumNameBox->setEditText(tag->album()); 0228 0229 fileNameBox->setText(item->file().fileInfo().fileName()); 0230 fileNameBox->setToolTip(item->file().absFilePath()); 0231 0232 bitrateBox->setText(QString::number(tag->bitrate())); 0233 lengthBox->setText(tag->lengthString()); 0234 0235 if(m_genreList.indexOf(tag->genre()) >= 0) 0236 genreBox->setCurrentIndex(m_genreList.indexOf(tag->genre()) + 1); 0237 else { 0238 genreBox->setCurrentIndex(0); 0239 genreBox->setEditText(tag->genre()); 0240 } 0241 0242 trackSpin->setValue(tag->track()); 0243 yearSpin->setValue(tag->year()); 0244 0245 commentBox->setPlainText(tag->comment()); 0246 0247 // Start at the second item, since we've already processed the first. 0248 0249 PlaylistItemList::Iterator it = m_items.begin(); 0250 ++it; 0251 0252 // If there is more than one item in the m_items that we're dealing with... 0253 0254 QList<QWidget *> disabledForMulti; 0255 0256 disabledForMulti << fileNameLabel << fileNameBox << lengthLabel << lengthBox 0257 << bitrateLabel << bitrateBox; 0258 0259 foreach(QWidget *w, disabledForMulti) { 0260 w->setDisabled(m_items.size() > 1); 0261 if(m_items.size() > 1 && !w->inherits("QLabel")) 0262 QMetaObject::invokeMethod(w, "clear"); 0263 } 0264 0265 if(it != m_items.end()) { 0266 0267 foreach(QCheckBox *box, m_enableBoxes) { 0268 box->setChecked(true); 0269 box->show(); 0270 } 0271 0272 // Yep, this is ugly. Loop through all of the files checking to see 0273 // if their fields are the same. If so, by default, enable their 0274 // checkbox. 0275 0276 // Also, if there are more than 50 m_items, don't scan all of them. 0277 0278 if(m_items.count() > 50) { 0279 m_enableBoxes[artistNameBox]->setChecked(false); 0280 m_enableBoxes[trackNameBox]->setChecked(false); 0281 m_enableBoxes[albumNameBox]->setChecked(false); 0282 m_enableBoxes[genreBox]->setChecked(false); 0283 m_enableBoxes[trackSpin]->setChecked(false); 0284 m_enableBoxes[yearSpin]->setChecked(false); 0285 m_enableBoxes[commentBox]->setChecked(false); 0286 } 0287 else { 0288 for(; it != m_items.end(); ++it) { 0289 tag = (*it)->file().tag(); 0290 0291 if(tag) { 0292 0293 if(artistNameBox->currentText() != tag->artist() && 0294 m_enableBoxes.contains(artistNameBox)) 0295 { 0296 artistNameBox->lineEdit()->clear(); 0297 m_enableBoxes[artistNameBox]->setChecked(false); 0298 } 0299 if(trackNameBox->text() != tag->title() && 0300 m_enableBoxes.contains(trackNameBox)) 0301 { 0302 trackNameBox->clear(); 0303 m_enableBoxes[trackNameBox]->setChecked(false); 0304 } 0305 if(albumNameBox->currentText() != tag->album() && 0306 m_enableBoxes.contains(albumNameBox)) 0307 { 0308 albumNameBox->lineEdit()->clear(); 0309 m_enableBoxes[albumNameBox]->setChecked(false); 0310 } 0311 if(genreBox->currentText() != tag->genre() && 0312 m_enableBoxes.contains(genreBox)) 0313 { 0314 genreBox->lineEdit()->clear(); 0315 m_enableBoxes[genreBox]->setChecked(false); 0316 } 0317 if(trackSpin->value() != tag->track() && 0318 m_enableBoxes.contains(trackSpin)) 0319 { 0320 trackSpin->setValue(0); 0321 m_enableBoxes[trackSpin]->setChecked(false); 0322 } 0323 if(yearSpin->value() != tag->year() && 0324 m_enableBoxes.contains(yearSpin)) 0325 { 0326 yearSpin->setValue(0); 0327 m_enableBoxes[yearSpin]->setChecked(false); 0328 } 0329 if(commentBox->toPlainText() != tag->comment() && 0330 m_enableBoxes.contains(commentBox)) 0331 { 0332 commentBox->clear(); 0333 m_enableBoxes[commentBox]->setChecked(false); 0334 } 0335 } 0336 } 0337 } 0338 } 0339 else { 0340 foreach(QCheckBox *box, m_enableBoxes) { 0341 box->setChecked(true); 0342 box->hide(); 0343 } 0344 } 0345 m_dataChanged = false; 0346 } 0347 0348 void TagEditor::slotClear() 0349 { 0350 artistNameBox->lineEdit()->clear(); 0351 trackNameBox->clear(); 0352 albumNameBox->lineEdit()->clear(); 0353 genreBox->setCurrentIndex(0); 0354 fileNameBox->clear(); 0355 fileNameBox->setToolTip(QString()); 0356 trackSpin->setValue(0); 0357 yearSpin->setValue(0); 0358 lengthBox->clear(); 0359 bitrateBox->clear(); 0360 commentBox->clear(); 0361 } 0362 0363 void TagEditor::slotUpdateCollection() 0364 { 0365 if(isVisible()) 0366 updateCollection(); 0367 else 0368 m_collectionChanged = true; 0369 } 0370 0371 void TagEditor::updateCollection() 0372 { 0373 m_collectionChanged = false; 0374 0375 CollectionList *list = CollectionList::instance(); 0376 0377 if(!list) 0378 return; 0379 0380 QStringList artistList = list->uniqueSet(CollectionList::Artists); 0381 artistList.sort(); 0382 artistNameBox->clear(); 0383 artistNameBox->addItems(artistList); 0384 artistNameBox->completionObject()->setItems(artistList); 0385 0386 QStringList albumList = list->uniqueSet(CollectionList::Albums); 0387 albumList.sort(); 0388 albumNameBox->clear(); 0389 albumNameBox->addItems(albumList); 0390 albumNameBox->completionObject()->setItems(albumList); 0391 0392 // Merge the list of genres found in tags with the standard ID3v1 set. 0393 0394 StringHash genreHash; 0395 0396 m_genreList = list->uniqueSet(CollectionList::Genres); 0397 0398 foreach(const QString &genre, m_genreList) 0399 genreHash.insert(genre); 0400 0401 TagLib::StringList genres = TagLib::ID3v1::genreList(); 0402 0403 for(TagLib::StringList::Iterator it = genres.begin(); it != genres.end(); ++it) 0404 genreHash.insert(TStringToQString((*it))); 0405 0406 m_genreList = genreHash.values(); 0407 m_genreList.sort(); 0408 0409 genreBox->clear(); 0410 genreBox->addItem(QString()); 0411 genreBox->addItems(m_genreList); 0412 genreBox->completionObject()->setItems(m_genreList); 0413 0414 // We've cleared out the original entries of these list boxes, re-read 0415 // the current item if one is selected. 0416 slotRefresh(); 0417 } 0418 0419 //////////////////////////////////////////////////////////////////////////////// 0420 // private members 0421 //////////////////////////////////////////////////////////////////////////////// 0422 0423 void TagEditor::readConfig() 0424 { 0425 // combo box completion modes 0426 0427 KConfigGroup config(KSharedConfig::openConfig(), "TagEditor"); 0428 if(artistNameBox && albumNameBox) { 0429 readCompletionMode(config, artistNameBox, "ArtistNameBoxMode"); 0430 readCompletionMode(config, albumNameBox, "AlbumNameBoxMode"); 0431 readCompletionMode(config, genreBox, "GenreBoxMode"); 0432 } 0433 0434 bool show = config.readEntry("Show", false); 0435 ActionCollection::action<KToggleAction>("showEditor")->setChecked(show); 0436 setVisible(show); 0437 0438 TagLib::StringList genres = TagLib::ID3v1::genreList(); 0439 0440 for(TagLib::StringList::ConstIterator it = genres.begin(); it != genres.end(); ++it) 0441 m_genreList.append(TStringToQString((*it))); 0442 m_genreList.sort(); 0443 0444 genreBox->clear(); 0445 genreBox->addItem(QString()); 0446 genreBox->addItems(m_genreList); 0447 genreBox->completionObject()->setItems(m_genreList); 0448 } 0449 0450 void TagEditor::readCompletionMode(const KConfigGroup &config, KComboBox *box, const QString &key) 0451 { 0452 KCompletion::CompletionMode mode = 0453 KCompletion::CompletionMode(config.readEntry(key, (int)KCompletion::CompletionAuto)); 0454 0455 box->setCompletionMode(mode); 0456 } 0457 0458 void TagEditor::saveConfig() 0459 { 0460 // combo box completion modes 0461 0462 KConfigGroup config(KSharedConfig::openConfig(), "TagEditor"); 0463 0464 if(artistNameBox && albumNameBox) { 0465 config.writeEntry("ArtistNameBoxMode", (int)artistNameBox->completionMode()); 0466 config.writeEntry("AlbumNameBoxMode", (int)albumNameBox->completionMode()); 0467 config.writeEntry("GenreBoxMode", (int)genreBox->completionMode()); 0468 } 0469 config.writeEntry("Show", ActionCollection::action<KToggleAction>("showEditor")->isChecked()); 0470 } 0471 0472 void TagEditor::setupActions() 0473 { 0474 using namespace IconSupport; 0475 0476 KToggleAction *show = new KToggleAction("document-properties"_icon, 0477 i18n("Show &Tag Editor"), this); 0478 ActionCollection::actions()->addAction("showEditor", show); 0479 connect(show, &QAction::toggled, this, &TagEditor::setVisible); 0480 0481 QAction *act = new QAction("document-save"_icon, i18n("&Save"), this); 0482 ActionCollection::actions()->addAction("saveItem", act); 0483 ActionCollection::actions()->setDefaultShortcut(act, 0484 QKeySequence(Qt::CTRL + Qt::Key_T)); 0485 connect(act, &QAction::triggered, this, &TagEditor::slotSave); 0486 } 0487 0488 void TagEditor::setupLayout() 0489 { 0490 setupUi(this); 0491 0492 // Do some meta-programming to find the matching enable boxes 0493 0494 const auto enableCheckBoxes = findChildren<QCheckBox *>(QRegularExpression("Enable$")); 0495 for(auto enable : enableCheckBoxes) { 0496 enable->hide(); // These are shown only when multiple items are being edited 0497 0498 // Each enable checkbox is identified by having its objectName end in "Enable". 0499 // The corresponding widget to be adjusted is identified by assigning a custom 0500 // property in Qt Designer "associatedObjectName", the value of which is the name 0501 // for the widget to be enabled (or not). 0502 auto associatedVariantValue = enable->property("associatedObjectName"); 0503 Q_ASSERT(associatedVariantValue.isValid()); 0504 0505 QWidget *associatedWidget = findChild<QWidget *>(associatedVariantValue.toString()); 0506 Q_ASSERT(associatedWidget != nullptr); 0507 0508 m_enableBoxes[associatedWidget] = enable; 0509 } 0510 0511 // Make sure that the labels are as tall as the enable boxes so that the 0512 // layout doesn't jump around as the enable boxes are shown/hidden. 0513 0514 const auto editorLabels = findChildren<QLabel *>(); 0515 for(auto label : editorLabels) { 0516 if(m_enableBoxes.contains(label->buddy())) 0517 label->setMinimumHeight(m_enableBoxes[label->buddy()]->height()); 0518 } 0519 0520 tagEditorLayout->setColumnMinimumWidth(1, 200); 0521 } 0522 0523 void TagEditor::save(const PlaylistItemList &list) 0524 { 0525 if(!list.isEmpty() && m_dataChanged) { 0526 0527 QApplication::setOverrideCursor(Qt::WaitCursor); 0528 m_dataChanged = false; 0529 m_performingSave = true; 0530 0531 // The list variable can become corrupted if the playlist holding its 0532 // items dies, which is possible as we edit tags. So we need to copy 0533 // the end marker. 0534 0535 PlaylistItemList::ConstIterator end = list.end(); 0536 0537 for(PlaylistItemList::ConstIterator it = list.begin(); it != end; /* Deliberately missing */ ) { 0538 0539 // Process items before we being modifying tags, as the dynamic 0540 // playlists will try to modify the file we edit if the tag changes 0541 // due to our alterations here. 0542 0543 qApp->processEvents(QEventLoop::ExcludeUserInputEvents); 0544 0545 PlaylistItem *item = *it; 0546 0547 // The playlist can be deleted from under us if this is the last 0548 // item and we edit it so that it doesn't match the search, which 0549 // means we can't increment the iterator, so let's do it now. 0550 0551 ++it; 0552 0553 QString fileName = item->file().fileInfo().path() + QDir::separator() + 0554 fileNameBox->text(); 0555 if(list.count() > 1) 0556 fileName = item->file().fileInfo().absoluteFilePath(); 0557 0558 Tag *tag = TagTransactionManager::duplicateTag(item->file().tag(), fileName); 0559 0560 // A bit more ugliness. If there are multiple files that are 0561 // being modified, they each have a "enabled" checkbox that 0562 // says if that field is to be respected for the multiple 0563 // files. We have to check to see if that is enabled before 0564 // each field that we write. 0565 0566 if(m_enableBoxes[artistNameBox]->isChecked()) 0567 tag->setArtist(artistNameBox->currentText()); 0568 if(m_enableBoxes[trackNameBox]->isChecked()) 0569 tag->setTitle(trackNameBox->text()); 0570 if(m_enableBoxes[albumNameBox]->isChecked()) 0571 tag->setAlbum(albumNameBox->currentText()); 0572 if(m_enableBoxes[trackSpin]->isChecked()) { 0573 if(trackSpin->text().isEmpty()) 0574 trackSpin->setValue(0); 0575 tag->setTrack(trackSpin->value()); 0576 } 0577 if(m_enableBoxes[yearSpin]->isChecked()) { 0578 if(yearSpin->text().isEmpty()) 0579 yearSpin->setValue(0); 0580 tag->setYear(yearSpin->value()); 0581 } 0582 if(m_enableBoxes[commentBox]->isChecked()) 0583 tag->setComment(commentBox->toPlainText()); 0584 0585 if(m_enableBoxes[genreBox]->isChecked()) 0586 tag->setGenre(genreBox->currentText()); 0587 0588 TagTransactionManager::instance()->changeTagOnItem(item, tag); 0589 } 0590 0591 TagTransactionManager::instance()->commit(); 0592 CollectionList::instance()->playlistItemsChanged(); 0593 m_performingSave = false; 0594 QApplication::restoreOverrideCursor(); 0595 } 0596 } 0597 0598 void TagEditor::saveChangesPrompt() 0599 { 0600 if(!isVisible() || !m_dataChanged || m_items.isEmpty()) 0601 return; 0602 0603 QStringList files; 0604 0605 foreach(const PlaylistItem *item, m_items) 0606 files.append(item->file().absFilePath()); 0607 0608 const auto questionFunc = 0609 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) 0610 &KMessageBox::questionTwoActionsList; 0611 #else 0612 &KMessageBox::questionYesNoList; 0613 #endif 0614 0615 const auto primaryResponse = 0616 #if KWIDGETSADDONS_VERSION >= QT_VERSION_CHECK(5, 100, 0) 0617 KMessageBox::PrimaryAction; 0618 #else 0619 KMessageBox::Yes; 0620 #endif 0621 0622 const auto response = questionFunc( 0623 this, 0624 i18n("Do you want to save your changes to:\n"), 0625 files, 0626 i18n("Save Changes"), 0627 KStandardGuiItem::save(), 0628 KStandardGuiItem::discard(), 0629 QStringLiteral("tagEditor_showSaveChangesBox"), 0630 KMessageBox::Notify 0631 ); 0632 0633 if(response == primaryResponse) { 0634 save(m_items); 0635 } 0636 } 0637 0638 void TagEditor::showEvent(QShowEvent *e) 0639 { 0640 if(m_collectionChanged) { 0641 updateCollection(); 0642 } 0643 0644 QWidget::showEvent(e); 0645 } 0646 0647 //////////////////////////////////////////////////////////////////////////////// 0648 // private slots 0649 //////////////////////////////////////////////////////////////////////////////// 0650 0651 void TagEditor::slotDataChanged() 0652 { 0653 m_dataChanged = true; 0654 } 0655 0656 void TagEditor::slotItemRemoved(PlaylistItem *item) 0657 { 0658 m_items.removeAll(item); 0659 if(m_items.isEmpty()) 0660 slotRefresh(); 0661 } 0662 0663 void TagEditor::slotPlaylistDestroyed(Playlist *p) 0664 { 0665 if(m_currentPlaylist == p) { 0666 m_currentPlaylist = 0; 0667 slotSetItems(PlaylistItemList()); 0668 } 0669 } 0670 0671 // vim: set et sw=4 tw=0 sta: