1 /* GUI_TagEdit.cpp */ 2 3 /* Copyright (C) 2011-2020 Michael Lugmair (Lucio Carreras) 4 * 5 * This file is part of sayonara player 6 * 7 * This program is free software: you can redistribute it and/or modify 8 * it under the terms of the GNU General Public License as published by 9 * the Free Software Foundation, either version 3 of the License, or 10 * (at your option) any later version. 11 12 * This program is distributed in the hope that it will be useful, 13 * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 * GNU General Public License for more details. 16 17 * You should have received a copy of the GNU General Public License 18 * along with this program. If not, see <http://www.gnu.org/licenses/>. 19 */ 20 21 #include "GUI_TagEdit.h" 22 #include "GUI_TagFromPath.h" 23 #include "GUI_CoverEdit.h" 24 #include "GUI_FailMessageBox.h" 25 26 #include "Gui/TagEdit/ui_GUI_TagEdit.h" 27 28 #include "Components/Tagging/Editor.h" 29 30 #include "Gui/Utils/Widgets/Completer.h" 31 #include "Gui/Utils/Style.h" 32 33 #include "Utils/Algorithm.h" 34 #include "Utils/Utils.h" 35 #include "Utils/Set.h" 36 #include "Utils/FileUtils.h" 37 #include "Utils/Message/Message.h" 38 #include "Utils/Tagging/Tagging.h" 39 #include "Utils/MetaData/MetaDataList.h" 40 #include "Utils/MetaData/Album.h" 41 #include "Utils/MetaData/Artist.h" 42 #include "Utils/Language/Language.h" 43 44 #include "Database/Connector.h" 45 #include "Database/LibraryDatabase.h" 46 47 #include <QFileInfo> 48 #include <QTabBar> 49 50 using namespace Tagging; 51 52 namespace 53 { 54 struct WidgetTuple 55 { 56 QLabel* label; 57 QWidget* widget; 58 QCheckBox* checkbox; 59 Lang::Term term; 60 }; 61 62 void setCheckboxText(QCheckBox* checkbox, int n) 63 { 64 if(checkbox) 65 { 66 const auto text = QString("%1 (%2)") 67 .arg(Lang::get(Lang::All)) 68 .arg(n); 69 70 checkbox->setText(text); 71 } 72 } 73 74 template<typename Container> 75 QStringList extractNames(const Container& container) 76 { 77 QStringList names; 78 for(const auto& item : container) 79 { 80 if(!item.name().isEmpty()) 81 { 82 names << item.name(); 83 } 84 } 85 86 return names; 87 } 88 89 Message::Answer showInvalidFilepathsMessage(int count, QObject* parent) 90 { 91 const auto message = QString("%1<br><br>%2") 92 .arg(parent->tr("Cannot apply expression to %n track(s)", "", count)) 93 .arg(parent->tr("Ignore these tracks?")); 94 95 return Message::question_yn(message); 96 } 97 98 Message::Answer showAllChangesLostMessage(QObject* parent) 99 { 100 const auto question = QString("%1.\n%2?") 101 .arg(parent->tr("All changes will be lost")) 102 .arg(Lang::get(Lang::Continue)); 103 104 return Message::question(question, "GUI_TagEdit", Message::QuestionType::YesNo); 105 } 106 107 void showFailedCommitMessageBox(const QMap<QString, Tagging::Editor::FailReason>& failedFiles, QWidget* parent) 108 { 109 auto* failMessageBox = new GUI_FailMessageBox(parent); 110 failMessageBox->setFailedFiles(failedFiles); 111 failMessageBox->setModal(true); 112 113 parent->connect(failMessageBox, &GUI_FailMessageBox::sigClosed, failMessageBox, &QObject::deleteLater); 114 failMessageBox->show(); 115 } 116 117 QStringList applyRegularExpression(int count, const QString& regex, Tagging::Editor* tagEditor) 118 { 119 QStringList invalidFilepaths; 120 121 for(auto i = 0; i < count; i++) 122 { 123 const auto success = tagEditor->applyRegularExpression(regex, i); 124 if(!success) 125 { 126 const auto invalidFilepath = tagEditor->metadata(i).filepath(); 127 invalidFilepaths << invalidFilepath; 128 } 129 } 130 131 return invalidFilepaths; 132 } 133 134 QCompleter* addCompleter(const QStringList& dataList, QLineEdit* lineEdit) 135 { 136 if(lineEdit->completer()) 137 { 138 lineEdit->completer()->deleteLater(); 139 } 140 141 auto* completer = new Gui::Completer(dataList, lineEdit); 142 lineEdit->setCompleter(completer); 143 144 return completer; 145 } 146 147 void initCompleter(Ui::GUI_TagEdit* ui) 148 { 149 auto* db = DB::Connector::instance(); 150 auto* libraryDatabase = db->libraryDatabase(-1, 0); 151 152 AlbumList albums; 153 ArtistList artists; 154 libraryDatabase->getAllAlbums(albums, true); 155 libraryDatabase->getAllArtists(artists, true); 156 const auto genres = libraryDatabase->getAllGenres(); 157 158 const auto albumNames = extractNames(albums); 159 const auto artistNames = extractNames(artists); 160 const auto genreNames = extractNames(genres); 161 162 addCompleter(albumNames, ui->leAlbum); 163 addCompleter(artistNames, ui->leAlbumArtist); 164 addCompleter(artistNames, ui->leArtist); 165 addCompleter(genreNames, ui->leGenre); 166 } 167 168 QPushButton* saveButton(Ui::GUI_TagEdit* ui) 169 { 170 return ui->buttonBox->button(QDialogButtonBox::StandardButton::Save); 171 } 172 } 173 174 struct GUI_TagEdit::Private 175 { 176 Tagging::Editor* tagEditor; 177 GUI_TagFromPath* uiTagFromPath; 178 GUI_CoverEdit* uiCoverEdit; 179 QList<WidgetTuple> widgetTuples; 180 int currentIndex; 181 182 Private(GUI_TagEdit* parent, Ui::GUI_TagEdit* ui) : 183 tagEditor {new Tagging::Editor()}, 184 uiTagFromPath {new GUI_TagFromPath(ui->tabFromPath)}, 185 uiCoverEdit {new GUI_CoverEdit(tagEditor, parent)}, 186 currentIndex {-1} 187 { 188 ui->tabFromPath->layout()->addWidget(uiTagFromPath); 189 ui->tabCover->layout()->addWidget(uiCoverEdit); 190 ui->tabWidget->setCurrentIndex(0); 191 ui->widgetRating->setMouseTrackable(false); 192 193 widgetTuples = QList<WidgetTuple> 194 {{ui->labTitle, ui->leTitle, nullptr, Lang::Title}, 195 {ui->labTrackNumber, ui->sbTrackNumber, nullptr, Lang::TrackNo}, 196 {ui->labAlbum, ui->leAlbum, ui->cbAlbumAll, Lang::Album}, 197 {ui->labArtist, ui->leArtist, ui->cbArtistAll, Lang::Artist}, 198 {ui->labAlbumArtist, ui->leAlbumArtist, ui->cbAlbumArtistAll, Lang::AlbumArtist}, 199 {ui->labGenres, ui->leGenre, ui->cbGenreAll, Lang::Genre}, 200 {ui->labYear, ui->sbYear, ui->cbYearAll, Lang::Year}, 201 {ui->labDiscnumber, ui->sbDiscnumber, ui->cbDiscnumberAll, Lang::Disc}, 202 {ui->labRatingText, ui->widgetRating, ui->cbRatingAll, Lang::Rating}, 203 {ui->labComment, ui->teComment, ui->cbCommentAll, Lang::Comment}}; 204 } 205 }; 206 207 GUI_TagEdit::GUI_TagEdit(QWidget* parent) : 208 Widget(parent) 209 { 210 ui = new Ui::GUI_TagEdit(); 211 ui->setupUi(this); 212 213 m = Pimpl::make<Private>(this, ui); 214 215 connect(m->tagEditor, &Editor::sigProgress, this, &GUI_TagEdit::progressChanged); 216 connect(m->tagEditor, &Editor::sigMetadataReceived, this, &GUI_TagEdit::metadataChanged); 217 connect(m->tagEditor, &Editor::sigStarted, this, &GUI_TagEdit::commitStarted); 218 connect(m->tagEditor, &Editor::sigFinished, this, &GUI_TagEdit::commitFinished); 219 220 for(const auto& widgetTuple : m->widgetTuples) 221 { 222 if(widgetTuple.checkbox) 223 { 224 connect(widgetTuple.checkbox, &QCheckBox::toggled, widgetTuple.widget, &QWidget::setDisabled); 225 } 226 } 227 228 connect(ui->btnNext, &QPushButton::clicked, this, &GUI_TagEdit::nextButtonClicked); 229 connect(ui->btnPrev, &QPushButton::clicked, this, &GUI_TagEdit::prevButtonClicked); 230 connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &GUI_TagEdit::commit); 231 connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &GUI_TagEdit::sigCancelled); 232 connect(ui->btnUndo, &QPushButton::clicked, this, &GUI_TagEdit::undoClicked); 233 connect(ui->btnUndoAll, &QPushButton::clicked, this, &GUI_TagEdit::undoAllClicked); 234 connect(ui->btnLoadCompleteAlbum, &QPushButton::clicked, this, &GUI_TagEdit::loadEntireAlbumClicked); 235 236 connect(m->uiTagFromPath, &GUI_TagFromPath::sigApply, this, &GUI_TagEdit::applyTagFromPathTriggered); 237 connect(m->uiTagFromPath, &GUI_TagFromPath::sigApplyAll, this, &GUI_TagEdit::applyAllTagFromPathTriggered); 238 239 metadataChanged(m->tagEditor->metadata()); 240 } 241 242 GUI_TagEdit::~GUI_TagEdit() = default; 243 244 void GUI_TagEdit::runEditor(Editor* editor) 245 { 246 auto* t = new QThread(); 247 editor->moveToThread(t); 248 249 connect(editor, &Tagging::Editor::sigFinished, t, &QThread::quit); 250 connect(editor, &Tagging::Editor::sigFinished, editor, [=]() { 251 editor->moveToThread(QApplication::instance()->thread()); 252 }); 253 254 connect(t, &QThread::started, editor, &Editor::commit); 255 connect(t, &QThread::finished, t, &QObject::deleteLater); 256 257 t->start(); 258 } 259 260 void GUI_TagEdit::languageChanged() 261 { 262 for(const auto& widgetTuple : m->widgetTuples) 263 { 264 widgetTuple.label->setText(Lang::get(widgetTuple.term)); 265 setCheckboxText(widgetTuple.checkbox, m->tagEditor->count()); 266 } 267 268 ui->btnLoadCompleteAlbum->setText(tr("Load complete album")); 269 ui->btnUndo->setText(Lang::get(Lang::Undo)); 270 ui->tabWidget->setTabText(0, tr("Metadata")); 271 ui->tabWidget->setTabText(1, tr("Tags from path")); 272 ui->tabWidget->setTabText(2, Lang::get(Lang::Covers)); 273 274 ui->retranslateUi(this); 275 } 276 277 void GUI_TagEdit::setMetadata(const MetaDataList& tracks) 278 { 279 m->tagEditor->setMetadata(tracks); 280 } 281 282 void GUI_TagEdit::metadataChanged([[maybe_unused]] const MetaDataList& changedTracks) 283 { 284 reset(); 285 286 for(const auto& widgetTuple : m->widgetTuples) 287 { 288 setCheckboxText(widgetTuple.checkbox, m->tagEditor->count()); 289 } 290 291 ui->btnLoadCompleteAlbum->setVisible(m->tagEditor->canLoadEntireAlbum()); 292 ui->btnLoadCompleteAlbum->setEnabled(true); 293 294 saveButton(ui)->setEnabled(true); 295 ui->btnUndo->setEnabled(true); 296 ui->btnUndoAll->setEnabled(true); 297 298 setCurrentIndex(0); 299 refreshCurrentTrack(); 300 } 301 302 void GUI_TagEdit::applyTagFromPathTriggered() 303 { 304 const auto success = m->tagEditor->applyRegularExpression(m->uiTagFromPath->getRegexString(), m->currentIndex); 305 if(success) 306 { 307 ui->tabWidget->setCurrentIndex(0); 308 } 309 310 refreshCurrentTrack(); 311 } 312 313 void GUI_TagEdit::applyAllTagFromPathTriggered() 314 { 315 const auto count = m->tagEditor->count(); 316 const auto regex = m->uiTagFromPath->getRegexString(); 317 const auto invalidFilepaths = applyRegularExpression(count, regex, m->tagEditor); 318 auto isValid = invalidFilepaths.isEmpty(); 319 320 if(!invalidFilepaths.isEmpty()) 321 { 322 for(const auto& invalidFilepath : invalidFilepaths) 323 { 324 this->m->uiTagFromPath->addInvalidFilepath(invalidFilepath); 325 } 326 327 const auto answer = showInvalidFilepathsMessage(invalidFilepaths.count(), this); 328 isValid = (answer == Message::Answer::Yes); 329 if(!isValid) 330 { 331 m->tagEditor->undoAll(); 332 } 333 } 334 335 if(isValid) 336 { 337 ui->tabWidget->setCurrentIndex(0); 338 } 339 340 refreshCurrentTrack(); 341 } 342 343 void GUI_TagEdit::setCurrentIndex(int index) 344 { 345 m->currentIndex = index; 346 m->uiCoverEdit->setCurrentIndex(index); 347 } 348 349 void GUI_TagEdit::nextButtonClicked() 350 { 351 writeChanges(m->currentIndex); 352 setCurrentIndex(m->currentIndex + 1); 353 refreshCurrentTrack(); 354 } 355 356 void GUI_TagEdit::prevButtonClicked() 357 { 358 writeChanges(m->currentIndex); 359 setCurrentIndex(m->currentIndex - 1); 360 refreshCurrentTrack(); 361 } 362 363 void GUI_TagEdit::refreshCurrentTrack() 364 { 365 const auto trackCount = m->tagEditor->count(); 366 const auto isNotLast = (m->currentIndex >= 0) && (m->currentIndex < trackCount - 1); 367 const auto isNotFirst = (m->currentIndex > 0) && (m->currentIndex < trackCount); 368 369 ui->btnNext->setEnabled(isNotLast); 370 ui->btnPrev->setEnabled(isNotFirst); 371 372 if(!Util::between(m->currentIndex, m->tagEditor->count())) 373 { 374 return; 375 } 376 377 const auto track = m->tagEditor->metadata(m->currentIndex); 378 379 { // set filepath label 380 const auto filepathLink = 381 Util::createLink(track.filepath(), Style::isDark(), true, Util::File::getParentDirectory(track.filepath())); 382 383 const auto fileInfo = QFileInfo(track.filepath()); 384 ui->labReadOnly->setVisible(!fileInfo.isWritable()); 385 ui->labFilepath->setText(filepathLink); 386 m->uiTagFromPath->setFilepath(track.filepath()); 387 } 388 389 ui->leTitle->setText(track.title()); 390 391 if(!ui->cbAlbumAll->isChecked()) 392 { 393 ui->leAlbum->setText(track.album()); 394 } 395 396 if(!ui->cbArtistAll->isChecked()) 397 { 398 ui->leArtist->setText(track.artist()); 399 } 400 401 if(!ui->cbAlbumArtistAll->isChecked()) 402 { 403 ui->leAlbumArtist->setText(track.albumArtist()); 404 } 405 406 if(!ui->cbGenreAll->isChecked()) 407 { 408 ui->leGenre->setText(track.genresToList().join(", ")); 409 } 410 411 if(!ui->cbYearAll->isChecked()) 412 { 413 ui->sbYear->setValue(track.year()); 414 } 415 416 if(!ui->cbDiscnumberAll->isChecked()) 417 { 418 ui->sbDiscnumber->setValue(track.discnumber()); 419 } 420 421 if(!ui->cbRatingAll->isChecked()) 422 { 423 ui->widgetRating->setRating(track.rating()); 424 } 425 426 if(!ui->cbCommentAll->isChecked()) 427 { 428 ui->teComment->setPlainText(track.comment()); 429 } 430 431 const auto isCoverSupported = m->tagEditor->isCoverSupported(m->currentIndex); 432 ui->tabCover->setEnabled(isCoverSupported); 433 if(!isCoverSupported) 434 { 435 ui->tabWidget->setCurrentIndex(0); 436 } 437 438 m->uiCoverEdit->refreshCurrentTrack(); 439 440 ui->sbTrackNumber->setValue(track.trackNumber()); 441 442 const auto trackIndexText = QString("%1 %2/%3") 443 .arg(Lang::get(Lang::Track).toFirstUpper()) 444 .arg(m->currentIndex + 1) 445 .arg(trackCount); 446 447 ui->labTrackIndex->setText(trackIndexText); 448 } 449 450 void GUI_TagEdit::reset() 451 { 452 setCurrentIndex(-1); 453 454 m->uiTagFromPath->reset(); 455 m->uiCoverEdit->reset(); 456 457 ui->tabWidget->tabBar()->setEnabled(true); 458 459 for(const auto& widgetTuple : m->widgetTuples) 460 { 461 if(widgetTuple.checkbox) 462 { 463 widgetTuple.checkbox->setChecked(false); 464 } 465 466 widgetTuple.widget->setEnabled(true); 467 } 468 469 ui->leTitle->clear(); 470 ui->labTrackIndex->setText(Lang::get(Lang::Track) + " 0/0"); 471 ui->sbTrackNumber->setValue(0); 472 ui->leAlbum->clear(); 473 ui->leArtist->clear(); 474 ui->leAlbumArtist->clear(); 475 ui->leGenre->clear(); 476 ui->sbYear->clear(); 477 ui->sbDiscnumber->clear(); 478 ui->teComment->clear(); 479 480 ui->btnPrev->setEnabled(false); 481 ui->btnNext->setEnabled(false); 482 483 ui->widgetRating->setRating(Rating::Zero); 484 485 ui->labFilepath->clear(); 486 ui->pbProgress->setVisible(false); 487 488 ui->btnLoadCompleteAlbum->setVisible(false); 489 490 initCompleter(ui); 491 } 492 493 void GUI_TagEdit::undoClicked() 494 { 495 m->tagEditor->undo(m->currentIndex); 496 refreshCurrentTrack(); 497 } 498 499 void GUI_TagEdit::undoAllClicked() 500 { 501 m->tagEditor->undoAll(); 502 refreshCurrentTrack(); 503 } 504 505 void GUI_TagEdit::writeChanges(int trackIndex) 506 { 507 if(!Util::between(m->currentIndex, m->tagEditor->count())) 508 { 509 return; 510 } 511 512 auto track = m->tagEditor->metadata(trackIndex); 513 514 track.setTitle(ui->leTitle->text()); 515 track.setArtist(ui->leArtist->text()); 516 track.setAlbum(ui->leAlbum->text()); 517 track.setAlbumArtist(ui->leAlbumArtist->text()); 518 track.setGenres(ui->leGenre->text().split(", ")); 519 track.setComment(ui->teComment->toPlainText()); 520 track.setDiscnumber(Disc(ui->sbDiscnumber->value())); 521 track.setYear(Year(ui->sbYear->value())); 522 track.setTrackNumber(TrackNum(ui->sbTrackNumber->value())); 523 track.setRating(ui->widgetRating->rating()); 524 525 const auto cover = m->uiCoverEdit->selectedCover(trackIndex); 526 527 m->tagEditor->updateTrack(trackIndex, track); 528 m->tagEditor->updateCover(trackIndex, cover); 529 } 530 531 void GUI_TagEdit::commit() 532 { 533 if(!saveButton(ui)->isEnabled()) 534 { 535 return; 536 } 537 538 writeChanges(m->currentIndex); 539 540 for(auto i = 0; i < m->tagEditor->count(); i++) 541 { 542 if(i == m->currentIndex) 543 { 544 continue; 545 } 546 547 auto track = m->tagEditor->metadata(i); 548 549 if(ui->cbAlbumAll->isChecked()) 550 { 551 track.setAlbum(ui->leAlbum->text()); 552 } 553 if(ui->cbArtistAll->isChecked()) 554 { 555 track.setArtist(ui->leArtist->text()); 556 } 557 if(ui->cbAlbumArtistAll->isChecked()) 558 { 559 track.setAlbumArtist(ui->leAlbumArtist->text()); 560 } 561 if(ui->cbGenreAll->isChecked()) 562 { 563 const auto genres = ui->leGenre->text().split(", "); 564 track.setGenres(genres); 565 } 566 567 if(ui->cbDiscnumberAll->isChecked()) 568 { 569 track.setDiscnumber(Disc(ui->sbDiscnumber->value())); 570 } 571 572 if(ui->cbRatingAll->isChecked()) 573 { 574 track.setRating(ui->widgetRating->rating()); 575 } 576 577 if(ui->cbYearAll->isChecked()) 578 { 579 track.setYear(Year(ui->sbYear->value())); 580 } 581 582 if(ui->cbCommentAll->isChecked()) 583 { 584 track.setComment(ui->teComment->toPlainText()); 585 } 586 587 m->tagEditor->updateTrack(i, track); 588 589 const auto cover = m->uiCoverEdit->selectedCover(i); 590 m->tagEditor->updateCover(i, cover); 591 } 592 593 runEditor(m->tagEditor); 594 } 595 596 void GUI_TagEdit::commitStarted() 597 { 598 saveButton(ui)->setEnabled(false); 599 ui->btnUndo->setEnabled(false); 600 ui->btnUndoAll->setEnabled(false); 601 ui->btnLoadCompleteAlbum->setEnabled(false); 602 603 ui->tabWidget->tabBar()->setEnabled(false); 604 605 ui->pbProgress->setVisible(true); 606 ui->pbProgress->setMinimum(0); 607 ui->pbProgress->setMaximum(100); 608 } 609 610 void GUI_TagEdit::progressChanged(int val) 611 { 612 if(val >= 0) 613 { 614 ui->pbProgress->setValue(val); 615 } 616 617 else 618 { 619 ui->pbProgress->setMinimum(0); 620 ui->pbProgress->setMaximum(0); 621 } 622 } 623 624 void GUI_TagEdit::commitFinished() 625 { 626 saveButton(ui)->setEnabled(true); 627 ui->btnLoadCompleteAlbum->setEnabled(m->tagEditor->canLoadEntireAlbum()); 628 ui->tabWidget->tabBar()->setEnabled(true); 629 ui->pbProgress->setVisible(false); 630 631 const auto failedFiles = m->tagEditor->failedFiles(); 632 if(!failedFiles.isEmpty()) 633 { 634 showFailedCommitMessageBox(failedFiles, this); 635 } 636 637 metadataChanged(m->tagEditor->metadata()); 638 } 639 640 void GUI_TagEdit::showDefaultTab() 641 { 642 ui->tabWidget->setCurrentIndex(0); 643 } 644 645 void GUI_TagEdit::showCoverTab() 646 { 647 ui->tabWidget->setCurrentIndex(2); 648 } 649 650 void GUI_TagEdit::loadEntireAlbumClicked() 651 { 652 if(m->tagEditor->hasChanges()) 653 { 654 const auto answer = showAllChangesLostMessage(this); 655 if(answer == Message::Answer::No) 656 { 657 return; 658 } 659 } 660 661 m->tagEditor->loadEntireAlbum(); 662 } 663 664 void GUI_TagEdit::showEvent(QShowEvent* e) 665 { 666 Widget::showEvent(e); 667 668 refreshCurrentTrack(); 669 ui->leTitle->setFocus(); 670 } 671