File indexing completed on 2021-12-21 13:28:01

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