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: