1 /*
2  * Cantata
3  *
4  * Copyright (c) 2011-2020 Craig Drummond <craig.p.drummond@gmail.com>
5  *
6  * ----
7  *
8  * This program is free software; you can redistribute it and/or modify
9  * it under the terms of the GNU General Public License as published by
10  * the Free Software Foundation; either version 2 of the License, or
11  * (at your option) any later version.
12  *
13  * This program is distributed in the hope that it will be useful,
14  * but WITHOUT ANY WARRANTY; without even the implied warranty of
15  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16  * General Public License for more details.
17  *
18  * You should have received a copy of the GNU General Public License
19  * along with this program; see the file COPYING.  If not, write to
20  * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21  * Boston, MA 02110-1301, USA.
22  */
23 
24 #include "tageditor.h"
25 #include "tags.h"
26 #include "widgets/tagspinbox.h"
27 #include "mpd-interface/mpdconnection.h"
28 #include "gui/settings.h"
29 #include "support/messagebox.h"
30 #include "support/inputdialog.h"
31 #include "trackorganiser.h"
32 #include "mpd-interface/cuefile.h"
33 #include "support/utils.h"
34 #include "support/monoicon.h"
35 #ifdef ENABLE_DEVICES_SUPPORT
36 #include "models/devicesmodel.h"
37 #include "devices/device.h"
38 #endif
39 #include <QMenu>
40 #include <QCloseEvent>
41 #include <QCoreApplication>
42 #include <QEventLoop>
43 #include <QDir>
44 #include <QTimer>
45 #include <algorithm>
46 
47 #define REMOVE(w) \
48     w->setVisible(false); \
49     w->deleteLater(); \
50     w=0;
51 
52 //
53 // NOTE: Cantata does NOT read, and store, comments from MPD. For comment support, Cantata will read these from the
54 // files when the tag editor is opened.
55 //
equalTags(const Song & a,const Song & b,bool compareCommon,bool composerSupport,bool commentSupport)56 static bool equalTags(const Song &a, const Song &b, bool compareCommon, bool composerSupport, bool commentSupport)
57 {
58     return (compareCommon || a.track==b.track) && a.year==b.year && a.disc==b.disc &&
59            a.artist==b.artist && a.genres[0]==b.genres[0] && a.album==b.album &&
60            a.albumartist==b.albumartist && (!composerSupport || a.composer()==b.composer()) &&
61            (!commentSupport || a.comment()==b.comment()) && (compareCommon || a.title==b.title);
62 }
63 
setString(QString & str,const QString & v,bool skipEmpty)64 static bool setString(QString &str, const QString &v, bool skipEmpty) {
65     if (!skipEmpty || !v.isEmpty()) {
66         str=v;
67         return true;
68     }
69     return false;
70 }
71 
trim(QString str)72 static QString trim(QString str) {
73     str=str.trimmed();
74     str=str.simplified();
75     str=str.replace(QLatin1String(" ;"), QLatin1String(";"));
76     str=str.replace(QLatin1String("; "), QLatin1String(";"));
77     return str;
78 }
79 
80 // Split genres back out
splitGenres(Song & song)81 static void splitGenres(Song &song) {
82     QStringList genres=song.genres[0].split(",", QString::SkipEmptyParts);
83     for (int i=0; i<Song::constNumGenres; ++i) {
84         song.genres[i]=i<genres.count() ? genres.at(i).trimmed() : QString();
85     }
86 }
87 
88 static int iCount=0;
89 
instanceCount()90 int TagEditor::instanceCount()
91 {
92     return iCount;
93 }
94 
nullRating(const quint8 & rating)95 static inline bool nullRating(const quint8 &rating)
96 {
97     return rating>Song::Rating_Max;
98 }
99 
nullRating(const Song & s)100 static inline bool nullRating(const Song &s)
101 {
102     return nullRating(s.rating);
103 }
104 
TagEditor(QWidget * parent,const QList<Song> & songs,const QSet<QString> & existingArtists,const QSet<QString> & existingAlbumArtists,const QSet<QString> & existingComposers,const QSet<QString> & existingAlbums,const QSet<QString> & existingGenres,const QString & udi)105 TagEditor::TagEditor(QWidget *parent, const QList<Song> &songs,
106                      const QSet<QString> &existingArtists, const QSet<QString> &existingAlbumArtists, const QSet<QString> &existingComposers,
107                      const QSet<QString> &existingAlbums, const QSet<QString> &existingGenres, const QString &udi)
108     : SongDialog(parent)
109     #ifdef ENABLE_DEVICES_SUPPORT
110     , deviceUdi(udi)
111     #endif
112     , currentSongIndex(-1)
113     , updating(false)
114     , haveArtists(false)
115     , haveAlbumArtists(false)
116     , haveComposers(false)
117     , haveComments(false)
118     , haveAlbums(false)
119     , haveGenres(false)
120     , haveDiscs(false)
121     , haveYears(false)
122     , haveRatings(false)
123     , saving(false)
124     , composerSupport(false)
125     , commentSupport(false)
126     , readRatingsAct(nullptr)
127     , writeRatingsAct(nullptr)
128 {
129     iCount++;
130     bool ratingsSupport=false;
131     #ifdef ENABLE_DEVICES_SUPPORT
132     if (deviceUdi.isEmpty()) {
133         baseDir=MPDConnection::self()->getDetails().dir;
134         composerSupport=MPDConnection::self()->composerTagSupported();
135         commentSupport=MPDConnection::self()->commentTagSupported();
136         ratingsSupport=MPDConnection::self()->stickersSupported();
137     } else {
138         Device *dev=getDevice(udi, parentWidget());
139 
140         if (!dev) {
141             deleteLater();
142             return;
143         }
144 
145         baseDir=dev->path();
146     }
147     #else
148     baseDir=MPDConnection::self()->getDetails().dir;
149     composerSupport=MPDConnection::self()->composerTagSupported();
150     commentSupport=MPDConnection::self()->commentTagSupported();
151     ratingsSupport=MPDConnection::self()->stickersSupported();
152     #endif
153 
154     for (const Song &s: songs) {
155         if (CueFile::isCue(s.file)) {
156             continue;
157         }
158         Song song(s);
159         song.rating=Song::Rating_Null;
160         if (s.guessed) {
161             song.revertGuessedTags();
162         }
163         // Store all Genres's in 1st genre
164         song.genres[0]=song.displayGenre();
165         song.genres[1]=QString();
166         original.append(song);
167         if (s.isLocalFile()) {
168             ratingsSupport = false;
169             composerSupport = true;
170             commentSupport = false;
171             baseDir = QString();
172         }
173     }
174 
175     if (original.isEmpty()) {
176         deleteLater();
177         return;
178     }
179     std::sort(original.begin(), original.end());
180 
181     if (!songsOk(original, baseDir, udi.isEmpty())) {
182         return;
183     }
184 
185     QWidget *mainWidet = new QWidget(this);
186     setupUi(mainWidet);
187     if (!ratingsSupport) {
188         REMOVE(ratingWidget);
189         REMOVE(ratingLabel);
190         REMOVE(ratingVarious);
191         REMOVE(ratingNoteLabel);
192     } else {
193         connect(this, SIGNAL(getRating(QString)), MPDConnection::self(), SLOT(getRating(QString)));
194         connect(this, SIGNAL(setRating(QString,quint8)), MPDConnection::self(), SLOT(setRating(QString,quint8)));
195         connect(MPDConnection::self(), SIGNAL(rating(QString,quint8)), this, SLOT(rating(QString,quint8)));
196         ratingWidget->setShowZeroForNull(true);
197         QColor col=palette().color(QPalette::WindowText);
198         ratingVarious->setStyleSheet(QString("QLabel{color:rgba(%1,%2,%3,128);}").arg(col.red()).arg(col.green()).arg(col.blue()));
199     }
200     setMainWidget(mainWidet);
201     ButtonCodes buttons=Ok|Cancel|Reset|User3;
202     if (songs.count()>1) {
203         buttons|=User2|User1;
204     }
205     setButtons(buttons);
206     setCaption(tr("Tags"));
207     if (songs.count()>1) {
208         setButtonGuiItem(User2, StdGuiItem::back(true));
209         setButtonGuiItem(User1,StdGuiItem::forward(true));
210         enableButton(User1, false);
211         enableButton(User2, false);
212     }
213     setButtonGuiItem(Ok, StdGuiItem::save());
214     setButtonGuiItem(User3, GuiItem(tr("Tools"), FontAwesome::magic));
215     QMenu *toolsMenu=new QMenu(this);
216     toolsMenu->addAction(tr("Apply \"Various Artists\" Workaround"), this, SLOT(applyVa()));
217     toolsMenu->addAction(tr("Revert \"Various Artists\" Workaround"), this, SLOT(revertVa()));
218     toolsMenu->addAction(tr("Set 'Album Artist' from 'Artist'"), this, SLOT(setAlbumArtistFromArtist()));
219     toolsMenu->addAction(tr("Capitalize"), this, SLOT(capitalise()));
220     toolsMenu->addAction(tr("Adjust Track Numbers"), this, SLOT(adjustTrackNumbers()));
221     if (ratingsSupport) {
222         readRatingsAct=toolsMenu->addAction(tr("Read Ratings from File"), this, SLOT(readRatings()));
223         writeRatingsAct=toolsMenu->addAction(tr("Write Ratings to File"), this, SLOT(writeRatings()));
224         readRatingsAct->setEnabled(false);
225         writeRatingsAct->setEnabled(false);
226     }
227     setButtonMenu(User3, toolsMenu, InstantPopup);
228     enableButton(Ok, false);
229     enableButton(Reset, false);
230 
231     setAttribute(Qt::WA_DeleteOnClose);
232 
233     QStringList strings=existingArtists.toList();
234     strings.sort();
235     artist->clear();
236     artist->insertItems(0, strings);
237 
238     strings=existingAlbumArtists.toList();
239     strings.sort();
240     albumArtist->clear();
241     albumArtist->insertItems(0, strings);
242 
243     if (composerSupport) {
244         strings=existingComposers.toList();
245         strings.sort();
246         composer->clear();
247         composer->insertItems(0, strings);
248     } else {
249         REMOVE(composer)
250         REMOVE(composerLabel)
251     }
252 
253     if (commentSupport) {
254         comment->clear();
255         composer->insertItems(0, strings);
256     } else {
257         REMOVE(comment)
258         REMOVE(commentLabel)
259     }
260 
261     strings=existingAlbums.toList();
262     strings.sort();
263     album->clear();
264     album->insertItems(0, strings);
265 
266     strings=existingGenres.toList();
267     strings.sort();
268     genre->clear();
269     genre->insertItems(0, strings);
270 
271     trackName->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Preferred);
272     trackName->setSizeAdjustPolicy(QComboBox::AdjustToMinimumContentsLength);
273     trackName->view()->setTextElideMode(Qt::ElideLeft);
274 
275     if (original.count()>1) {
276         QSet<QString> songArtists;
277         QSet<QString> songAlbumArtists;
278         QSet<QString> songAlbums;
279         QSet<QString> songGenres;
280         QSet<QString> songComposers;
281         QSet<QString> songComments;
282         QSet<int> songYears;
283         QSet<int> songDiscs;
284 
285         for (const Song &s: original) {
286             songArtists.insert(s.artist);
287             songAlbumArtists.insert(s.albumartist);
288             songAlbums.insert(s.album);
289             for (int i=0; i<Song::constNumGenres && !s.genres[i].isEmpty(); ++i) {
290                 songGenres.insert(s.genres[i]);
291             }
292             if (composerSupport) {
293                 songComposers.insert(s.composer());
294             }
295             if (commentSupport) {
296                 songComments.insert(s.comment());
297             }
298             songYears.insert(s.year);
299             songDiscs.insert(s.disc);
300             if (songArtists.count()>1 && songAlbumArtists.count()>1 && songAlbums.count()>1 &&
301                 songGenres.count()>1 && songYears.count()>1 && songDiscs.count()>1 &&
302                 (!composerSupport || songComposers.count()>1) && (!commentSupport || songComments.count()>1)) {
303                 break;
304             }
305         }
306         Song all;
307         all.file.clear();
308         all.title.clear();
309         all.track=0;
310         all.artist=1==songArtists.count() ? *(songArtists.begin()) : QString();
311         if (1==songComposers.count()) {
312             all.setComposer(*(songComposers.begin()));
313         }
314         all.setComment(1==songComments.count() ? *(songComments.begin()) : QString());
315         all.albumartist=1==songAlbumArtists.count() ? *(songAlbumArtists.begin()) : QString();
316         all.album=1==songAlbums.count() ? *(songAlbums.begin()) : QString();
317         all.genres[0]=1==songGenres.count() ? *(songGenres.begin()) : QString();
318         all.year=1==songYears.count() ? *(songYears.begin()) : 0;
319         all.disc=1==songDiscs.count() ? *(songDiscs.begin()) : 0;
320         all.rating=Song::Rating_Null;
321         original.prepend(all);
322         artist->setFocus();
323         haveArtists=songArtists.count()>1;
324         haveAlbumArtists=songAlbumArtists.count()>1;
325         haveAlbums=songAlbums.count()>1;
326         haveGenres=songGenres.count()>1;
327         haveComposers=songComposers.count()>1;
328         haveDiscs=songDiscs.count()>1;
329         haveYears=songYears.count()>1;
330     } else {
331         title->setFocus();
332     }
333     edited=original;
334     setIndex(0);
335 
336     if (ratingsSupport || commentSupport) {
337         progress->setVisible(true);
338         progress->setRange(0, (original.count()>1 ? (original.count()-1) : 1)*(commentSupport && ratingsSupport ? 2 : 1));
339         progress->setValue(0);
340     } else {
341         progress->setVisible(false);
342     }
343 
344     bool first=original.count()>1;
345     for (const Song &s: original) {
346         if (first) {
347             trackName->insertItem(trackName->count(), tr("All tracks"));
348             first=false;
349         } else {
350             trackName->insertItem(trackName->count(), s.filePath());
351             if (ratingsSupport) {
352                 emit getRating(s.file);
353             }
354         }
355     }
356     connect(title, SIGNAL(textChanged(const QString &)), SLOT(checkChanged()));
357     connect(artist, SIGNAL(activated(int)), SLOT(checkChanged()));
358     connect(artist, SIGNAL(editTextChanged(const QString &)), SLOT(checkChanged()));
359     connect(albumArtist, SIGNAL(activated(int)), SLOT(checkChanged()));
360     connect(albumArtist, SIGNAL(editTextChanged(const QString &)), SLOT(checkChanged()));
361     if (composerSupport) {
362         connect(composer, SIGNAL(activated(int)), SLOT(checkChanged()));
363         connect(composer, SIGNAL(editTextChanged(const QString &)), SLOT(checkChanged()));
364     }
365     if (commentSupport) {
366         connect(comment, SIGNAL(textChanged(const QString &)), SLOT(checkChanged()));
367     }
368     connect(album, SIGNAL(activated(int)), SLOT(checkChanged()));
369     connect(album, SIGNAL(editTextChanged(const QString &)), SLOT(checkChanged()));
370     connect(genre, SIGNAL(activated(int)), SLOT(checkChanged()));
371     connect(track, SIGNAL(valueChanged(int)), SLOT(checkChanged()));
372     connect(disc, SIGNAL(valueChanged(int)), SLOT(checkChanged()));
373     connect(genre, SIGNAL(editTextChanged(const QString &)), SLOT(checkChanged()));
374     connect(year, SIGNAL(valueChanged(int)), SLOT(checkChanged()));
375     connect(trackName, SIGNAL(activated(int)), SLOT(setIndex(int)));
376     connect(this, SIGNAL(update()), MPDConnection::self(), SLOT(updateMaybe()));
377     if (ratingWidget) {
378         connect(ratingWidget, SIGNAL(valueChanged(int)), SLOT(checkRating()));
379     }
380     adjustSize();
381     int w=Utils::scaleForDpi(600);
382     if (width()<w) {
383         resize(w, height());
384     }
385     if (commentSupport) {
386         QTimer::singleShot(0, this, SLOT(readComments()));
387     }
388     trackName->lineEdit()->setReadOnly(true);
389 }
390 
~TagEditor()391 TagEditor::~TagEditor()
392 {
393     iCount--;
394 }
395 
fillSong(Song & s,bool isAll,bool skipEmpty) const396 void TagEditor::fillSong(Song &s, bool isAll, bool skipEmpty) const
397 {
398     Song all=original.at(0);
399     bool haveAll=original.count()>1;
400 
401     if (!isAll) {
402         setString(s.title, title->text().trimmed(), skipEmpty);
403     }
404     setString(s.artist, artist->text().trimmed(), skipEmpty && (!haveAll || all.artist.isEmpty()));
405     setString(s.album, album->text().trimmed(), skipEmpty && (!haveAll || all.album.isEmpty()));
406     setString(s.albumartist, albumArtist->text().trimmed(), skipEmpty && (!haveAll || all.albumartist.isEmpty()));
407     if (composerSupport) {
408         QString str;
409         if (setString(str, composer->text().trimmed(), skipEmpty && (!haveAll || all.composer().isEmpty()))) {
410             s.setComposer(str);
411         }
412     }
413     if (commentSupport) {
414         QString str;
415         if (setString(str, comment->text().trimmed(), skipEmpty && (!haveAll || all.comment().isEmpty()))) {
416             s.setComment(str);
417         }
418     }
419     if (!isAll) {
420         s.track=track->value();
421     }
422     if (!isAll || 0!=disc->value()) {
423         s.disc=disc->value();
424     }
425     setString(s.genres[0], trim(genre->text()), skipEmpty && (!haveAll || all.genres[0].isEmpty()));
426     if (!isAll || 0!=year->value()) {
427         s.year=year->value();
428     }
429     if (ratingWidget) {
430         s.rating=ratingWidget->value();
431     }
432 }
433 
setVariousHint()434 void TagEditor::setVariousHint()
435 {
436     if (0==currentSongIndex && original.count()>1) {
437         Song all=original.at(0);
438         artist->setPlaceholderText(all.artist.isEmpty() && haveArtists ? TagSpinBox::variousStr() : QString());
439         album->setPlaceholderText(all.album.isEmpty() && haveAlbums ? TagSpinBox::variousStr() : QString());
440         albumArtist->setPlaceholderText(all.albumartist.isEmpty() && haveAlbumArtists ? TagSpinBox::variousStr() : QString());
441         if (composerSupport) {
442             composer->setPlaceholderText(all.composer().isEmpty() && haveComposers ? TagSpinBox::variousStr() : QString());
443         }
444         if (commentSupport) {
445             comment->setPlaceholderText(all.comment().isEmpty() && haveComments ? TagSpinBox::variousStr() : QString());
446         }
447         genre->setPlaceholderText(all.genres[0].isEmpty() && haveGenres ? TagSpinBox::variousStr() : QString());
448         disc->setVarious(0==all.disc && haveDiscs);
449         year->setVarious(0==all.year && haveYears);
450         if (ratingVarious) {
451             ratingVarious->setVisible(nullRating(all) && haveRatings);
452         }
453     } else {
454         artist->setPlaceholderText(QString());
455         album->setPlaceholderText(QString());
456         albumArtist->setPlaceholderText(QString());
457         if (composerSupport) {
458             composer->setPlaceholderText(QString());
459         }
460         if (commentSupport) {
461             comment->setPlaceholderText(QString());
462         }
463         genre->setPlaceholderText(QString());
464         disc->setVarious(false);
465         year->setVarious(false);
466         if (ratingVarious) {
467             ratingVarious->setVisible(false);
468         }
469     }
470 }
471 
enableOkButton()472 void TagEditor::enableOkButton()
473 {
474     enableButton(Ok, (editedIndexes.count()>1) ||
475                      (1==original.count() && 1==editedIndexes.count()) ||
476                      (1==editedIndexes.count() && !editedIndexes.contains(0)) );
477     enableButton(Reset, isButtonEnabled(Ok));
478 }
479 
setLabelStates()480 void TagEditor::setLabelStates()
481 {
482     Song o=original.at(currentSongIndex);
483     Song e=edited.at(currentSongIndex);
484     bool isAll=0==currentSongIndex && original.count()>1;
485 
486     titleLabel->setOn(!isAll && o.title!=e.title);
487     artistLabel->setOn(o.artist!=e.artist);
488     if (composerSupport) {
489         composerLabel->setOn(o.composer()!=e.composer());
490     }
491     if (commentSupport) {
492         commentLabel->setOn(o.comment()!=e.comment());
493     }
494     albumArtistLabel->setOn(o.albumartist!=e.albumartist);
495     albumLabel->setOn(o.album!=e.album);
496     trackLabel->setOn(!isAll && o.track!=e.track);
497     discLabel->setOn(o.disc!=e.disc);
498     genreLabel->setOn(o.genres[0]!=e.genres[0]);
499     yearLabel->setOn(o.year!=e.year);
500     if (ratingLabel) {
501         ratingLabel->setOn(o.rating<=Song::Rating_Max &&
502                            e.rating<=Song::Rating_Max &&
503                            o.rating!=e.rating);
504     }
505 }
506 
readComments()507 void TagEditor::readComments()
508 {
509     bool haveMultiple=original.count()>1;
510     bool updated=false;
511     bool multipleComments=false;
512     QString allComment;
513 
514     for (int i=0; i<original.count(); ++i) {
515         if (0!=i || !haveMultiple) {
516             progress->setValue(progress->value()+1);
517         }
518         if (i && 0==i%10) {
519             QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
520         }
521 
522         if (0==i && haveMultiple) {
523             continue;
524         }
525         Song song=original.at(i);
526         QString comment=Tags::readComment(song.filePath(baseDir));
527         if (!comment.isEmpty()) {
528             song.setComment(comment);
529             original.replace(i, song);
530             haveComments=true;
531             updated=true;
532             if (haveMultiple) {
533                 if (allComment.isEmpty()) {
534                     allComment=comment;
535                 } else if (comment!=allComment) {
536                     multipleComments=true;
537                 }
538             }
539         }
540     }
541     if (updated) {
542         edited=original;
543         if (haveMultiple && !allComment.isEmpty() && !multipleComments) {
544             edited[0].setComment(allComment);
545             original[0].setComment(allComment);
546             if (0==currentSongIndex) {
547                 comment->setText(allComment);
548             }
549         }
550     }
551     controlInitialActionsState();
552     if (haveMultiple) {
553         setVariousHint();
554     } else {
555         setSong(original.at(0));
556     }
557 }
558 
applyVa()559 void TagEditor::applyVa()
560 {
561     bool isAll=0==currentSongIndex && original.count()>1;
562 
563     if (MessageBox::No==MessageBox::questionYesNo(this, (isAll ? tr("Apply \"Various Artists\" workaround to <b>all</b> tracks?")
564                                                                : tr("Apply \"Various Artists\" workaround?"))+
565                                                            QLatin1String("<br/><br/>")+
566                                                            tr("<i>This will set 'Album artist' and 'Artist' to "
567                                                                 "\"Various Artists\", and set 'Title' to "
568                                                                 "\"TrackArtist - TrackTitle\"</i>"), tr("Apply \"Various Artists\" Workaround"),
569                                                   StdGuiItem::apply(), StdGuiItem::cancel())) {
570         return;
571     }
572 
573     if (isAll) {
574         updating=true;
575         for (int i=0; i<edited.count(); ++i) {
576             Song s=edited.at(i);
577             if (s.fixVariousArtists()) {
578                 if (0==i && QLatin1String(" - ")==s.title) {
579                     s.title=QString();
580                 }
581                 edited.replace(i, s);
582                 updateEditedStatus(i);
583                 if (i==currentSongIndex) {
584                     setSong(s);
585                 }
586             }
587         }
588         updating=false;
589         setLabelStates();
590         enableOkButton();
591     } else {
592         Song s=edited.at(currentSongIndex);
593         if (s.fixVariousArtists()) {
594             edited.replace(currentSongIndex, s);
595             updateEditedStatus(currentSongIndex);
596             setSong(s);
597         }
598     }
599 }
600 
revertVa()601 void TagEditor::revertVa()
602 {
603     bool isAll=0==currentSongIndex && original.count()>1;
604 
605     if (MessageBox::No==MessageBox::questionYesNo(this, (isAll ? tr("Revert \"Various Artists\" workaround on <b>all</b> tracks?")
606                                                                : tr("Revert \"Various Artists\" workaround"))+
607                                                            QLatin1String("<br/><br/>")+
608                                                            tr("<i>Where the 'Album artist' is the same as 'Artist' "
609                                                                 "and the 'Title' is of the format \"TrackArtist - TrackTitle\", "
610                                                                 "'Artist' will be taken from 'Title' and 'Title' itself will be "
611                                                                 "set to just the title. e.g. <br/><br/>"
612                                                                 "If 'Title' is \"Wibble - Wobble\", then 'Artist' will be set to "
613                                                                 "\"Wibble\" and 'Title' will be set to \"Wobble\"</i>"), tr("Revert \"Various Artists\" Workaround"),
614                                                   GuiItem(tr("Revert")), StdGuiItem::cancel())) {
615         return;
616     }
617 
618     if (isAll) {
619         updating=true;
620         QSet<QString> artists;
621         for (int i=1; i<edited.count(); ++i) {
622             Song s=edited.at(i);
623             if (s.revertVariousArtists()) {
624                 artists.insert(s.artist);
625                 edited.replace(i, s);
626                 updateEditedStatus(i);
627                 if (i==currentSongIndex) {
628                     setSong(s);
629                 }
630             }
631         }
632         Song s=edited.at(0);
633         s.artist=artists.count()!=1 ? QString() : *artists.constBegin();
634         edited.replace(0, s);
635         updateEditedStatus(0);
636         setSong(s);
637         updating=false;
638         setLabelStates();
639         enableOkButton();
640     } else {
641         Song s=edited.at(currentSongIndex);
642         if (s.revertVariousArtists()) {
643             edited.replace(currentSongIndex, s);
644             updateEditedStatus(currentSongIndex);
645             setSong(s);
646             enableOkButton();
647         }
648     }
649 }
650 
setAlbumArtistFromArtist()651 void TagEditor::setAlbumArtistFromArtist()
652 {
653     bool isAll=0==currentSongIndex && original.count()>1;
654 
655     if (MessageBox::No==MessageBox::questionYesNo(this, isAll ? tr("Set 'Album Artist' from 'Artist' (if 'Album Artist' is empty) for <b>all</b> tracks?")
656                                                               : tr("Set 'Album Artist' from 'Artist' (if 'Album Artist' is empty)?"),
657                                                   tr("Album Artist from Artist"), StdGuiItem::apply(), StdGuiItem::cancel())) {
658         return;
659     }
660 
661     if (isAll) {
662         updating=true;
663         for (int i=0; i<edited.count(); ++i) {
664             Song s=edited.at(i);
665             if (s.setAlbumArtist()) {
666                 edited.replace(i, s);
667                 updateEditedStatus(i);
668                 if (i==currentSongIndex) {
669                     setSong(s);
670                 }
671             }
672         }
673         updating=false;
674         setLabelStates();
675         enableOkButton();
676     } else {
677         Song s=edited.at(currentSongIndex);
678         if (s.setAlbumArtist()) {
679             edited.replace(currentSongIndex, s);
680             updateEditedStatus(currentSongIndex);
681             setSong(s);
682         }
683     }
684 }
685 
capitalise()686 void TagEditor::capitalise()
687 {
688     bool isAll=0==currentSongIndex && original.count()>1;
689 
690     if (MessageBox::No==MessageBox::questionYesNo(this, isAll ? tr("Capitalize the first letter of text fields (e.g. 'Title', 'Artist', etc) "
691                                                                      "of <b>all</b> tracks?")
692                                                               : tr("Capitalize the first letter of text fields (e.g. 'Title', 'Artist', etc)?"),
693                                                   tr("Capitalize"), GuiItem(tr("Capitalize")), StdGuiItem::cancel())) {
694         return;
695     }
696 
697     if (isAll) {
698         for (int i=0; i<edited.count(); ++i) {
699             Song s=edited.at(i);
700             if (s.capitalise()) {
701                 edited.replace(i, s);
702                 updateEditedStatus(i);
703                 if (i==currentSongIndex) {
704                     setSong(s);
705                 }
706             }
707         }
708     } else {
709         Song s=edited.at(currentSongIndex);
710         if (s.capitalise()) {
711             edited.replace(currentSongIndex, s);
712             updateEditedStatus(currentSongIndex);
713             setSong(s);
714         }
715     }
716 }
717 
adjustTrackNumbers()718 void TagEditor::adjustTrackNumbers()
719 {
720     bool isAll=0==currentSongIndex && original.count()>1;
721     bool ok=false;
722     int adj=InputDialog::getInteger(tr("Adjust Track Numbers"), isAll ? tr("Adjust the value of each track number by:")
723                                                                         : tr("Adjust track number by:"),
724                                     0, -500, 500, 1, 10, &ok, this);
725 
726     if (!ok || 0==adj) {
727         return;
728     }
729 
730     if (isAll) {
731         for (int i=1; i<edited.count(); ++i) {
732             Song s=edited.at(i);
733             s.track+=adj;
734             edited.replace(i, s);
735             updateEditedStatus(i);
736             if (i==currentSongIndex) {
737                 setSong(s);
738             }
739         }
740     } else {
741         Song s=edited.at(currentSongIndex);
742         s.track+=adj;
743         edited.replace(currentSongIndex, s);
744         updateEditedStatus(currentSongIndex);
745         setSong(s);
746     }
747 }
748 
readRatings()749 void TagEditor::readRatings()
750 {
751     bool isAll=0==currentSongIndex && original.count()>1;
752 
753     if (MessageBox::No==MessageBox::questionYesNo(this, isAll ? tr("Read ratings for all tracks from the music files?")
754                                                               : tr("Read rating from music file?"),
755                                                   tr("Ratings"),
756                                                   isAll ? GuiItem(tr("Read Ratings")) : GuiItem(tr("Read Rating")),
757                                                   StdGuiItem::cancel())) {
758         return;
759     }
760 
761     if (isAll) {
762         progress->setVisible(true);
763         progress->setRange(0, original.count());
764         QStringList updated;
765         for (int i=1; i<original.count(); ++i) {
766             progress->setValue(i+1);
767             if (i && 0==i%10) {
768                 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
769             }
770             Song s=edited.at(i);
771             int r=Tags::readRating(s.filePath(baseDir));
772             if (r>=0 && r<=Song::Rating_Max && s.rating!=r) {
773                 s.rating=r;
774                 edited.replace(i, s);
775                 updated.append(s.file);
776                 editedIndexes.insert(i);
777                 updateTrackName(i, true);
778                 if (i==currentSongIndex) {
779                     setSong(s);
780                 }
781             }
782         }
783         progress->setVisible(false);
784         if (!updated.isEmpty()) {
785             MessageBox::informationList(this, tr("Read, and updated, ratings from the following tracks:"), updated);
786         }
787     } else {
788         Song s=edited.at(currentSongIndex);
789         int r=Tags::readRating(s.filePath(baseDir));
790         if (r>=0 && r<=Song::Rating_Max && s.rating!=r) {
791             s.rating=r;
792             edited.replace(currentSongIndex, s);
793             setSong(s);
794         }
795     }
796     enableOkButton();
797 }
798 
writeRatings()799 void TagEditor::writeRatings()
800 {
801     bool isAll=0==currentSongIndex && original.count()>1;
802 
803     if (isAll) {
804         for (int i=1; i<original.count(); ++i) {
805             if (nullRating(original.at(i))) {
806                 MessageBox::error(this, tr("Not all Song ratings have been read from MPD!")+QLatin1String("\n\n")+
807                                   tr("Song ratings are not stored in the song files, but within MPD's 'sticker' database. "
808                                        "In order to save these into the actual file, Cantata must first read them from MPD."));
809                 return;
810             }
811         }
812     } else {
813         if (nullRating(original.at(currentSongIndex))) {
814             MessageBox::error(this, tr("Song rating has not been read from MPD!")+QLatin1String("\n\n")+
815                               tr("Song ratings are not stored in the song files, but within MPD's 'sticker' database. "
816                                    "In order to save these into the actual file, Cantata must first read them from MPD."));
817             return;
818         }
819     }
820 
821     if (MessageBox::No==MessageBox::questionYesNo(this, isAll ? tr("Write ratings for all tracks to the music files?")
822                                                               : tr("Write rating to music file?"),
823                                                   tr("Ratings"),
824                                                   isAll ? GuiItem(tr("Write Ratings")) : GuiItem(tr("Write Rating")),
825                                                   StdGuiItem::cancel())) {
826         return;
827     }
828 
829     if (isAll) {
830         progress->setVisible(true);
831         progress->setRange(0, edited.count());
832         QStringList failed;
833         for (int i=1; i<edited.count(); ++i) {
834             progress->setValue(i+1);
835             if (i && 0==i%10) {
836                 QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
837             }
838             Song s=edited.at(i);
839             if (s.rating<=Song::Rating_Max) {
840                 Tags::Update status=Tags::updateRating(s.filePath(baseDir), s.rating);
841                 if (Tags::Update_Failed==status || Tags::Update_BadFile==status){
842                     failed.append(s.file);
843                 }
844             }
845         }
846         progress->setVisible(false);
847         if (!failed.isEmpty()) {
848             MessageBox::errorList(this, tr("Failed to write ratings of the following tracks:"), failed);
849         }
850     } else {
851         Song s=edited.at(currentSongIndex);
852         if (s.rating<=Song::Rating_Max) {
853             Tags::Update status=Tags::updateRating(s.filePath(baseDir), s.rating);
854             if (Tags::Update_Failed==status || Tags::Update_BadFile==status){
855                 MessageBox::error(this, tr("Failed to write rating to music file!"));
856             }
857         }
858     }
859 }
860 
checkChanged()861 void TagEditor::checkChanged()
862 {
863     if (updating) {
864         return;
865     }
866 
867     bool allWasEdited=editedIndexes.contains(0);
868 
869     updateEdited();
870 
871     bool allEdited=editedIndexes.contains(0);
872     bool isAll=0==currentSongIndex && original.count()>1;
873 
874     if (isAll && (allEdited || allWasEdited)) {
875         int save=currentSongIndex;
876         for (int i=1; i<edited.count(); ++i) {
877             currentSongIndex=i;
878             updateEdited(true);
879         }
880         currentSongIndex=save;
881     }
882     enableOkButton();
883     setLabelStates();
884 }
885 
updateTrackName(int index,bool edited)886 void TagEditor::updateTrackName(int index, bool edited)
887 {
888     bool isAll=0==index && original.count()>1;
889 
890     if (edited) {
891         if (isAll) {
892             trackName->setItemText(index, tr("All tracks [modified]"));
893         } else {
894             trackName->setItemText(index, tr("%1 [modified]").arg(original.at(index).filePath()));
895         }
896     } else {
897         if (isAll) {
898             trackName->setItemText(index, tr("All tracks"));
899         } else {
900             trackName->setItemText(index, original.at(index).filePath());
901         }
902     }
903 }
904 
updateEditedStatus(int index)905 void TagEditor::updateEditedStatus(int index)
906 {
907     bool isAll=0==index && original.count()>1;
908     Song s=edited.at(index);
909     if (equalTags(s, original.at(index), isAll, composerSupport, commentSupport) && s.rating==original.at(index).rating) {
910         if (editedIndexes.contains(index)) {
911             editedIndexes.remove(index);
912             updateTrackName(index, false);
913             edited.replace(index, s);
914         }
915     } else {
916         if (!editedIndexes.contains(index)) {
917             editedIndexes.insert(index);
918             updateTrackName(index, true);
919         }
920         edited.replace(index, s);
921     }
922 }
923 
updateEdited(bool isFromAll)924 void TagEditor::updateEdited(bool isFromAll)
925 {
926     Song s=edited.at(currentSongIndex);
927     bool isAll=0==currentSongIndex && original.count()>1;
928     fillSong(s, isFromAll || isAll, /*isFromAll*/false);
929 
930     if (!isAll && isFromAll && original.count()>1) {
931         Song all=original.at(0);
932         Song o=original.at(currentSongIndex);
933         if (all.artist.isEmpty() && s.artist.isEmpty() && !o.artist.isEmpty()) {
934             s.artist=o.artist;
935         }
936         if (all.albumartist.isEmpty() && s.albumartist.isEmpty() && !o.albumartist.isEmpty()) {
937             s.albumartist=o.albumartist;
938         }
939         if (composerSupport && all.composer().isEmpty() && s.composer().isEmpty() && !o.composer().isEmpty()) {
940             s.setComposer(o.composer());
941         }
942         if (commentSupport && all.comment().isEmpty() && s.comment().isEmpty() && !o.comment().isEmpty()) {
943             s.setComment(o.comment());
944         }
945         if (all.album.isEmpty() && s.album.isEmpty() && !o.album.isEmpty()) {
946             s.album=o.album;
947         }
948         if (all.genres[0].isEmpty() && s.genres[0].isEmpty() && !o.genres[0].isEmpty()) {
949             s.genres[0]=o.genres[0];
950         }
951     }
952 
953     if (equalTags(s, original.at(currentSongIndex), isFromAll || isAll, composerSupport, commentSupport) && s.rating==original.at(currentSongIndex).rating) {
954         if (editedIndexes.contains(currentSongIndex)) {
955             editedIndexes.remove(currentSongIndex);
956             updateTrackName(currentSongIndex, false);
957             edited.replace(currentSongIndex, s);
958         }
959     } else {
960         if (!editedIndexes.contains(currentSongIndex)) {
961             editedIndexes.insert(currentSongIndex);
962             updateTrackName(currentSongIndex, true);
963         }
964         edited.replace(currentSongIndex, s);
965     }
966 }
967 
setSong(const Song & s)968 void TagEditor::setSong(const Song &s)
969 {
970     blockSignals(true);
971     title->setText(s.title);
972     artist->setText(s.artist);
973     albumArtist->setText(s.albumartist);
974     if (composerSupport) {
975         composer->setText(s.composer());
976     }
977     if (commentSupport) {
978         comment->setText(s.comment());
979     }
980     album->setText(s.album);
981     track->setValue(s.track);
982     disc->setValue(s.disc);
983     genre->setText(s.genres[0]);
984     year->setValue(s.year);
985     if (ratingWidget) {
986         ratingWidget->setValue(s.rating);
987     }
988     blockSignals(false);
989     checkChanged();
990 }
991 
setIndex(int idx)992 void TagEditor::setIndex(int idx)
993 {
994     if (currentSongIndex==idx || idx>(original.count()-1)) {
995         return;
996     }
997     updating=true;
998 
999     bool haveMultiple=original.count()>1;
1000 
1001     if (haveMultiple && currentSongIndex>=0) {
1002         updateEdited();
1003     }
1004     Song s=edited.at(!haveMultiple || idx==0 ? 0 : idx);
1005     setSong(s);
1006     currentSongIndex=idx;
1007 
1008     bool isMultiple=haveMultiple && 0==idx;
1009     title->setEnabled(!isMultiple);
1010     titleLabel->setEnabled(!isMultiple);
1011     track->setEnabled(!isMultiple);
1012     trackLabel->setEnabled(!isMultiple);
1013 
1014     if (isMultiple) {
1015         title->setText(QString());
1016         track->setValue(0);
1017     }
1018 
1019     if (original.count()>1) {
1020         enableButton(User1, !isMultiple && idx<(original.count()-1)); // Next
1021         enableButton(User2, !isMultiple && idx>1); // Prev
1022     }
1023     setVariousHint();
1024     enableOkButton();
1025     trackName->setCurrentIndex(idx);
1026     setLabelStates();
1027     updating=false;
1028 }
1029 
rating(const QString & f,quint8 r)1030 void TagEditor::rating(const QString &f, quint8 r)
1031 {
1032     if (!ratingWidget) {
1033         return;
1034     }
1035     for (int i=original.count()>1 ? 1 : 0; i<original.count(); ++i) {
1036         Song s=original.at(i);
1037         if (nullRating(s) && s.rating!=r && s.file==f) {
1038             progress->setValue(progress->value()+1);
1039             controlInitialActionsState();
1040             s.rating=r;
1041             original.replace(i, s);
1042             s=edited.at(i);
1043             if (nullRating(s)) {
1044                 s.rating=r;
1045                 edited.replace(i, s);
1046             }
1047             if (i==currentSongIndex && ratingWidget->value()>Song::Rating_Max) {
1048                 ratingWidget->setValue(r);
1049             }
1050         }
1051     }
1052 
1053     if (original.count()>1 && !haveRatings) {
1054         quint8 rating=Song::Rating_Null;
1055         bool first=true;
1056         for (int i=1; i<original.count() && !haveRatings; ++i) {
1057             quint8 r=original.at(i).rating;
1058             if (nullRating(r)) {
1059                 continue;
1060             }
1061             if (first) {
1062                 rating=r;
1063                 first=false;
1064             } else if (r!=rating) {
1065                 haveRatings=true;
1066                 rating=Song::Rating_Null;
1067             }
1068         }
1069         Song s=original.at(0);
1070         if (s.rating!=rating) {
1071             s.rating=rating;
1072             original.replace(0, s);
1073         }
1074         s=edited.at(0);
1075         if (s.rating!=rating) {
1076             s.rating=rating;
1077             edited.replace(0, s);
1078         }
1079         if (0==currentSongIndex) {
1080             ratingWidget->setValue(rating);
1081         }
1082         setVariousHint();
1083     }
1084 }
1085 
checkRating()1086 void TagEditor::checkRating()
1087 {
1088     if (!ratingWidget) {
1089         return;
1090     }
1091     checkChanged();
1092     if (original.count()>1 && 0==currentSongIndex) {
1093         quint8 rating=Song::Rating_Null;
1094         bool first=true;
1095         haveRatings=false;
1096         for (int i=1; i<edited.count() && !haveRatings; ++i) {
1097             quint8 r=edited.at(i).rating;
1098             if (nullRating(r)) {
1099                 continue;
1100             }
1101             if (first) {
1102                 rating=r;
1103                 first=false;
1104             } else if (r!=rating) {
1105                 haveRatings=true;
1106                 rating=Song::Rating_Null;
1107             }
1108         }
1109         Song s=edited.at(0);
1110         if (s.rating!=rating) {
1111             s.rating=rating;
1112             edited.replace(0, s);
1113             if (0==currentSongIndex) {
1114                 ratingWidget->setValue(rating);
1115             }
1116         }
1117         setVariousHint();
1118     }
1119 }
1120 
applyUpdates()1121 bool TagEditor::applyUpdates()
1122 {
1123     bool skipFirst=original.count()>1;
1124     QStringList failed;
1125     DeviceOptions opts;
1126     QString udi;
1127     #ifdef ENABLE_DEVICES_SUPPORT
1128     Device * dev=nullptr;
1129     if (!deviceUdi.isEmpty()) {
1130         dev=getDevice(deviceUdi, this);
1131         if (!dev) {
1132             return true;
1133         }
1134         opts=dev->options();
1135         udi=dev->id();
1136     } else
1137     #endif
1138     opts.load(MPDConnectionDetails::configGroupName(MPDConnection::self()->getDetails().name), true);
1139 
1140     int toSave=editedIndexes.count();
1141     bool renameFiles=false;
1142     QList<Song> updatedSongs;
1143 
1144     saving=true;
1145     enableButton(Ok, false);
1146     enableButton(Cancel, false);
1147     enableButton(Reset, false);
1148     enableButton(User1, false);
1149     enableButton(User2, false);
1150     enableButton(User3, false);
1151     progress->setVisible(true);
1152     progress->setRange(1, toSave);
1153 
1154     int count=0;
1155     bool someTimedout=false;
1156     bool isLocal=false;
1157     for (int idx: editedIndexes) {
1158         progress->setValue(progress->value()+1);
1159 
1160         if (0==count++%10) {
1161             QCoreApplication::processEvents(QEventLoop::ExcludeUserInputEvents);
1162         }
1163 
1164         if (skipFirst && 0==idx) {
1165             continue;
1166         }
1167         Song orig=original.at(idx);
1168         Song edit=edited.at(idx);
1169 
1170         if (orig.isLocalFile()) {
1171             isLocal = true;
1172         }
1173         if (ratingWidget && orig.rating!=edit.rating && edit.rating<=Song::Rating_Max) {
1174             emit setRating(orig.file, edit.rating);
1175         }
1176 
1177         if (equalTags(orig, edit, false, composerSupport, commentSupport)) {
1178             continue;
1179         }
1180 
1181         QString file=orig.filePath();
1182         QString afile=orig.filePath(baseDir);
1183         splitGenres(orig);
1184         splitGenres(edit);
1185         switch(Tags::update(afile, orig, edit, -1, commentSupport)) {
1186         case Tags::Update_Modified:
1187             edit.setComment(QString());
1188             #ifdef ENABLE_DEVICES_SUPPORT
1189             if (!deviceUdi.isEmpty()) {
1190                 if (!dev->updateSong(orig, edit)) {
1191                     dev->removeSongFromList(orig);
1192                     dev->addSongToList(edit);
1193                 }
1194             } else
1195             #endif
1196             {
1197 //                if (!MusicLibraryModel::self()->updateSong(orig, edit)) {
1198 //                    MusicLibraryModel::self()->removeSongFromList(orig);
1199 //                    MusicLibraryModel::self()->addSongToList(edit);
1200 //                }
1201             }
1202             updatedSongs.append(edit);
1203             if (!renameFiles && !isLocal && file!=opts.createFilename(edit)) {
1204                 renameFiles=true;
1205             }
1206             break;
1207         case Tags::Update_Failed:
1208             failed.append(file);
1209             break;
1210         case Tags::Update_BadFile:
1211             failed.append(tr("%1 (Corrupt tags?)", "filename (Corrupt tags?)").arg(file));
1212             break;
1213         default:
1214             break;
1215         }
1216     }
1217     saving=false;
1218 
1219     if (failed.count()) {
1220         MessageBox::errorListEx(this, tr("Failed to update the tags of the following tracks:"), failed);
1221     }
1222 
1223     if (updatedSongs.count()) {
1224         // If we call tag-editor, no need to do MPD update - as this will be done from that dialog...
1225         if (renameFiles &&
1226             MessageBox::Yes==MessageBox::questionYesNo(this, tr("Would you also like to rename your song files, so as to match your tags?"),
1227                                                        tr("Rename Files"), GuiItem(tr("Rename")), StdGuiItem::cancel())) {
1228             TrackOrganiser *dlg=new TrackOrganiser(parentWidget());
1229             dlg->show(updatedSongs, udi, true);
1230         } else {
1231             #ifdef ENABLE_DEVICES_SUPPORT
1232             if (!deviceUdi.isEmpty()) {
1233                 dev->saveCache();
1234             } else
1235             #endif
1236             if (!isLocal) {
1237             //             MusicLibraryModel::self()->removeCache();
1238                 emit update();
1239             }
1240         }
1241     }
1242 
1243     return !someTimedout;
1244 }
1245 
slotButtonClicked(int button)1246 void TagEditor::slotButtonClicked(int button)
1247 {
1248     switch (button) {
1249     case Ok: {
1250         if (applyUpdates()) {
1251             accept();
1252         }
1253         break;
1254     }
1255     case Reset: // Reset
1256         if (0==currentSongIndex && original.count()>1) {
1257             for (int i=0; i<original.count(); ++i) {
1258                 edited.replace(i, original.at(i));
1259                 updateTrackName(i, false);
1260             }
1261             editedIndexes.clear();
1262             setSong(original.at(currentSongIndex));
1263             checkRating();
1264         } else {
1265             setSong(original.at(currentSongIndex));
1266         }
1267         setLabelStates();
1268         enableOkButton();
1269         break;
1270     case User1: // Next
1271         setIndex(currentSongIndex+1);
1272         break;
1273     case User2: // Prev
1274         setIndex(currentSongIndex-1);
1275         break;
1276     case Cancel:
1277         reject();
1278         // Need to call this - if not, when dialog is closed by window X control, it is not deleted!!!!
1279         Dialog::slotButtonClicked(button);
1280         break;
1281     default:
1282         break;
1283     }
1284 }
1285 
1286 #ifdef ENABLE_DEVICES_SUPPORT
getDevice(const QString & udi,QWidget * p)1287 Device * TagEditor::getDevice(const QString &udi, QWidget *p)
1288 {
1289     Device *dev=DevicesModel::self()->device(udi);
1290     if (!dev) {
1291         MessageBox::error(p ? p : this, tr("Device has been removed!"));
1292         reject();
1293         return nullptr;
1294     }
1295     if (!dev->isConnected()) {
1296         MessageBox::error(p ? p : this, tr("Device is not connected."));
1297         reject();
1298         return nullptr;
1299     }
1300     if (!dev->isIdle()) {
1301         MessageBox::error(p ? p : this, tr("Device is busy?"));
1302         reject();
1303         return nullptr;
1304     }
1305     return dev;
1306 }
1307 #endif
1308 
closeEvent(QCloseEvent * event)1309 void TagEditor::closeEvent(QCloseEvent *event)
1310 {
1311     if (saving) {
1312         event->ignore();
1313     } else {
1314         Dialog::closeEvent(event);
1315     }
1316 }
1317 
controlInitialActionsState()1318 void TagEditor::controlInitialActionsState()
1319 {
1320     if (progress->value()>=progress->maximum()) {
1321         progress->setVisible(false);
1322         if (readRatingsAct) {
1323             readRatingsAct->setEnabled(true);
1324         }
1325         if (writeRatingsAct) {
1326             writeRatingsAct->setEnabled(true);
1327         }
1328     }
1329 }
1330 
1331 #include "moc_tageditor.cpp"
1332