1 /* BEGIN_COMMON_COPYRIGHT_HEADER
2  * (c)LGPL2+
3  *
4  * Flacon - audio File Encoder
5  * https://github.com/flacon/flacon
6  *
7  * Copyright: 2012-2013
8  *   Alexander Sokoloff <sokoloff.a@gmail.com>
9  *
10  * This library is free software; you can redistribute it and/or
11  * modify it under the terms of the GNU Lesser General Public
12  * License as published by the Free Software Foundation; either
13  * version 2.1 of the License, or (at your option) any later version.
14 
15  * This library is distributed in the hope that it will be useful,
16  * but WITHOUT ANY WARRANTY; without even the implied warranty of
17  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
18  * Lesser General Public License for more details.
19 
20  * You should have received a copy of the GNU Lesser General Public
21  * License along with this library; if not, write to the Free Software
22  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
23  *
24  * END_COMMON_COPYRIGHT_HEADER */
25 
26 #include "mainwindow.h"
27 #include "project.h"
28 #include "disc.h"
29 #include "settings.h"
30 #include "converter/converter.h"
31 #include "formats_out/outformat.h"
32 #include "inputaudiofile.h"
33 #include "formats_in/informat.h"
34 #include "preferences/preferencesdialog.h"
35 #include "aboutdialog/aboutdialog.h"
36 #include "scanner.h"
37 #include "gui/coverdialog/coverdialog.h"
38 #include "internet/dataprovider.h"
39 #include "gui/trackviewmodel.h"
40 #include "gui/tageditor/tageditor.h"
41 #include "controls.h"
42 #include "gui/icon.h"
43 #include "application.h"
44 #include "patternexpander.h"
45 
46 #include <QFileDialog>
47 #include <QDir>
48 #include <QDebug>
49 #include <QMessageBox>
50 #include <QTextCodec>
51 #include <QQueue>
52 #include <QKeyEvent>
53 #include <QMimeData>
54 #include <QStyleFactory>
55 #include <QToolBar>
56 #include <QToolButton>
57 #include <QStandardPaths>
58 
59 #ifdef MAC_UPDATER
60 #include "updater/updater.h"
61 #endif
62 
63 /************************************************
64 
65  ************************************************/
MainWindow(QWidget * parent)66 MainWindow::MainWindow(QWidget *parent) :
67     QMainWindow(parent),
68     mScanner(nullptr)
69 {
70     Messages::setHandler(this);
71 
72     setupUi(this);
73 
74     qApp->setWindowIcon(loadMainIcon());
75     toolBar->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
76     toolBar->setIconSize(QSize(24, 24));
77     qApp->setAttribute(Qt::AA_DontShowIconsInMenus, true);
78     qApp->setAttribute(Qt::AA_UseHighDpiPixmaps, true);
79 
80 #ifdef Q_OS_MAC
81     this->setUnifiedTitleAndToolBarOnMac(true);
82     setWindowIcon(QIcon());
83 
84     trackView->setFrameShape(QFrame::NoFrame);
85     splitter->setStyleSheet("::handle{ border-right: 1px solid #7F7F7F7F;}");
86 #endif
87 
88     setAcceptDrops(true);
89     setAcceptDrops(true);
90     this->setContextMenuPolicy(Qt::NoContextMenu);
91 
92     outPatternButton->setToolTip(outPatternEdit->toolTip());
93     outDirButton->setToolTip(outDirEdit->toolTip());
94 
95     // TrackView ...............................................
96     trackView->setRootIsDecorated(false);
97     trackView->setItemsExpandable(false);
98     trackView->hideColumn((int)TrackView::ColumnComment);
99     trackView->setAlternatingRowColors(false);
100 
101     // Tag edits ...............................................
102     tagGenreEdit->setTagId(TagId::Genre);
103     connect(tagGenreEdit, &TagLineEdit::textEdited, this, &MainWindow::setTrackTag);
104 
105     tagYearEdit->setTagId(TagId::Date);
106     connect(tagYearEdit, &TagLineEdit::textEdited, this, &MainWindow::setTrackTag);
107 
108     tagArtistEdit->setTagId(TagId::Artist);
109     connect(tagArtistEdit, &TagLineEdit::textEdited, this, &MainWindow::setTrackTag);
110     connect(tagArtistEdit, &TagLineEdit::textEdited, this, &MainWindow::refreshEdits);
111 
112     tagDiscPerformerEdit->setTagId(TagId::AlbumArtist);
113     connect(tagDiscPerformerEdit, &TagLineEdit::textEdited, this, &MainWindow::setDiscTag);
114     connect(tagDiscPerformerEdit, &TagLineEdit::textEdited, this, &MainWindow::refreshEdits);
115 
116     tagAlbumEdit->setTagId(TagId::Album);
117     connect(tagAlbumEdit, &TagLineEdit::textEdited, this, &MainWindow::setTrackTag);
118 
119     tagDiscIdEdit->setTagId(TagId::DiscId);
120     connect(tagDiscIdEdit, &TagLineEdit::textEdited, this, &MainWindow::setTrackTag);
121     connect(tagDiscIdEdit, &TagLineEdit::textEdited, this, &MainWindow::setControlsEnable);
122 
123     connect(tagStartNumEdit, &MultiValuesSpinBox::editingFinished, this, &MainWindow::setStartTrackNum);
124     connect(tagStartNumEdit, qOverload<int>(&MultiValuesSpinBox::valueChanged),
125             this, &MainWindow::setStartTrackNum);
126 
127     connect(trackView->model(), &TrackViewModel::dataChanged, this, &MainWindow::refreshEdits);
128     connect(trackView, &TrackView::customContextMenuRequested, this, &MainWindow::trackViewMenu);
129 
130     connect(editTagsButton, &QPushButton::clicked, this, &MainWindow::openEditTagsDialog);
131 
132     initActions();
133 
134     // Buttons .................................................
135     outDirButton->setBuddy(outDirEdit);
136     outDirButton->menu()->addSeparator();
137     QAction *act = outDirEdit->deleteItemAction();
138     act->setText(tr("Remove current directory from history"));
139     outDirButton->menu()->addAction(act);
140 
141     configureProfileBtn->setBuddy(outProfileCombo);
142     configureProfileBtn->setDefaultAction(actionConfigureEncoder);
143 
144     outPatternButton->setBuddy(outPatternEdit);
145     outPatternButton->addStandardPatterns();
146     outPatternButton->menu()->addSeparator();
147 
148     outPatternEdit->deleteItemAction()->setText(tr("Delete current pattern from history"));
149     outPatternButton->menu()->addAction(outPatternEdit->deleteItemAction());
150 
151     loadSettings();
152 
153     // Signals .................................................
154     connect(Settings::i(), &Settings::changed,
155             [this]() { trackView->model()->layoutChanged(); });
156 
157     connect(outDirEdit->lineEdit(), &QLineEdit::textChanged, this, &MainWindow::setOutDir);
158     connect(outPatternEdit->lineEdit(), &QLineEdit::textChanged, this, &MainWindow::setPattern);
159 
160     connect(outProfileCombo, qOverload<int>(&QComboBox::currentIndexChanged),
161             this, &MainWindow::setOutProfile);
162 
163     connect(codepageCombo, qOverload<int>(&QComboBox::currentIndexChanged),
164             this, &MainWindow::setCodePage);
165 
166     connect(trackView, &TrackView::selectCueFile, this, &MainWindow::setCueForDisc);
167     connect(trackView, &TrackView::selectAudioFile, this, &MainWindow::setAudioForDisc);
168     connect(trackView, &TrackView::showAudioMenu, this, &MainWindow::showDiskAudioFileMenu);
169     connect(trackView, &TrackView::selectCoverImage, this, &MainWindow::setCoverImage);
170     connect(trackView, &TrackView::downloadInfo, this, &MainWindow::downloadDiscInfo);
171 
172     connect(trackView->model(), &TrackViewModel::layoutChanged, this, &MainWindow::refreshEdits);
173     connect(trackView->model(), &TrackViewModel::layoutChanged, this, &MainWindow::setControlsEnable);
174 
175     connect(trackView->selectionModel(), &QItemSelectionModel::selectionChanged,
176             this, &MainWindow::refreshEdits);
177     connect(trackView->selectionModel(), &QItemSelectionModel::selectionChanged,
178             this, &MainWindow::setControlsEnable);
179 
180     connect(project, &Project::layoutChanged, trackView, &TrackView::layoutChanged);
181     connect(project, &Project::layoutChanged, this, &MainWindow::refreshEdits);
182     connect(project, &Project::layoutChanged, this, &MainWindow::setControlsEnable);
183 
184     connect(project, &Project::discChanged, this, &MainWindow::refreshEdits);
185     connect(project, &Project::discChanged, this, &MainWindow::setControlsEnable);
186 
187     connect(Application::instance(), &Application::visualModeChanged,
188             []() {
189                 Icon::setDarkMode(Application::instance()->isDarkVisualMode());
190             });
191 
192     Icon::setDarkMode(Application::instance()->isDarkVisualMode());
193 
194     refreshEdits();
195     setControlsEnable();
196     polishView();
197 }
198 
199 /************************************************
200  *
201  ************************************************/
202 #ifdef Q_OS_MAC
polishView()203 void MainWindow::polishView()
204 {
205     QList<QLabel *> labels;
206     labels << outFilesBox->findChildren<QLabel *>();
207     labels << tagsBox->findChildren<QLabel *>();
208 
209     for (QLabel *label : labels) {
210         label->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
211     }
212 }
213 #else
polishView()214 void MainWindow::polishView()
215 {
216 }
217 #endif
218 
219 /************************************************
220  *
221  ************************************************/
showEvent(QShowEvent *)222 void MainWindow::showEvent(QShowEvent *)
223 {
224     if (project->count())
225         trackView->selectDisc(project->disc(0));
226 }
227 
228 /************************************************
229 
230  ************************************************/
~MainWindow()231 MainWindow::~MainWindow()
232 {
233 }
234 
235 /************************************************
236 
237  ************************************************/
closeEvent(QCloseEvent *)238 void MainWindow::closeEvent(QCloseEvent *)
239 {
240     if (mConverter)
241         mConverter->stop();
242     saveSettings();
243 }
244 
245 /************************************************
246 
247  ************************************************/
dragEnterEvent(QDragEnterEvent * event)248 void MainWindow::dragEnterEvent(QDragEnterEvent *event)
249 {
250     foreach (QUrl url, event->mimeData()->urls()) {
251         if (url.isLocalFile()) {
252             event->acceptProposedAction();
253             return;
254         }
255     }
256 }
257 
258 /************************************************
259 
260  ************************************************/
dropEvent(QDropEvent * event)261 void MainWindow::dropEvent(QDropEvent *event)
262 {
263     foreach (QUrl url, event->mimeData()->urls()) {
264         addFileOrDir(url.toLocalFile());
265     }
266 }
267 
268 /************************************************
269 
270  ************************************************/
setPattern()271 void MainWindow::setPattern()
272 {
273     Settings::i()->currentProfile().setOutFilePattern(outPatternEdit->currentText());
274     trackView->model()->layoutChanged();
275 }
276 
277 /************************************************
278 
279  ************************************************/
setOutDir()280 void MainWindow::setOutDir()
281 {
282     Settings::i()->currentProfile().setOutFileDir(outDirEdit->currentText());
283     trackView->model()->layoutChanged();
284 }
285 
286 /************************************************
287 
288  ************************************************/
setCueForDisc(Disc * disc)289 void MainWindow::setCueForDisc(Disc *disc)
290 {
291     QString flt = getOpenFileFilter(false, true);
292 
293     QString dir;
294     {
295         QStringList audioFiles = disc->audioFilePaths();
296         audioFiles.removeAll("");
297 
298         if (!audioFiles.isEmpty()) {
299             dir = QFileInfo(audioFiles.first()).dir().absolutePath();
300         }
301         else if (!disc->cueFilePath().isEmpty()) {
302             dir = QFileInfo(disc->cueFilePath()).dir().absolutePath();
303         }
304         else {
305             dir = Settings::i()->value(Settings::Misc_LastDir).toString();
306         }
307     }
308 
309     QString fileName = QFileDialog::getOpenFileName(this, tr("Select CUE file", "OpenFile dialog title"), dir, flt);
310 
311     if (fileName.isEmpty())
312         return;
313 
314     try {
315         Cue     cue(fileName);
316         QString oldDir = QFileInfo(disc->cueFilePath()).dir().path();
317         disc->setCueFile(cue);
318         QString newDir = QFileInfo(disc->cueFilePath()).dir().path();
319 
320         if (disc->isMultiAudio()) {
321             disc->searchAudioFiles(false);
322         }
323 
324         if (newDir != oldDir) {
325             disc->searchCoverImage(false);
326         }
327     }
328     catch (FlaconError &err) {
329         Messages::error(err.what());
330     }
331 }
332 
333 /************************************************
334 
335  ************************************************/
setOutProfile()336 void MainWindow::setOutProfile()
337 {
338     int n = outProfileCombo->currentIndex();
339     if (n > -1) {
340         Settings::i()->selectProfile(outProfileCombo->itemData(n).toString());
341     }
342     trackView->model()->layoutChanged();
343 }
344 
345 /************************************************
346 
347  ************************************************/
setControlsEnable()348 void MainWindow::setControlsEnable()
349 {
350     bool convert = mConverter && mConverter->isRunning();
351     bool scan    = mScanner;
352     bool running = scan || convert;
353 
354     bool tracksSelected = !trackView->selectedTracks().isEmpty();
355     bool discsSelected  = !trackView->selectedDiscs().isEmpty();
356     bool canConvert     = Conv::Converter::canConvert();
357 
358     bool canDownload = false;
359     foreach (const Disc *disc, trackView->selectedDiscs())
360         canDownload = canDownload || !disc->discId().isEmpty();
361 
362     outFilesBox->setEnabled(!running);
363     tagsBox->setEnabled(!running && tracksSelected);
364     actionAddDisc->setEnabled(!running);
365     actionRemoveDisc->setEnabled(!running && discsSelected);
366     actionStartConvert->setEnabled(!running && canConvert);
367     actionDownloadTrackInfo->setEnabled(!running && canDownload);
368     actionAddFolder->setEnabled(!running);
369     actionConfigure->setEnabled(!running);
370     actionConfigureEncoder->setEnabled(!running);
371 
372     actionStartConvert->setEnabled(!scan && canConvert);
373     actionAbortConvert->setEnabled(convert);
374 
375     actionStartConvert->setVisible(!convert);
376     actionAbortConvert->setVisible(convert);
377 }
378 
379 /************************************************
380 
381  ************************************************/
refreshEdits()382 void MainWindow::refreshEdits()
383 {
384 
385     // Discs ...............................
386     QSet<int>     startNums;
387     QSet<QString> discId;
388     QSet<QString> codePage;
389 
390     QList<Disc *> discs = trackView->selectedDiscs();
391     foreach (Disc *disc, discs) {
392         startNums << disc->startTrackNum();
393         discId << disc->discId();
394         codePage << disc->codecName();
395     }
396 
397     // Tracks ..............................
398     QSet<QString> genre;
399     QSet<QString> artist;
400     QSet<QString> album;
401     QSet<QString> date;
402     QSet<QString> discPerformer;
403 
404     QList<Track *> tracks = trackView->selectedTracks();
405     foreach (Track *track, tracks) {
406         genre << track->genre();
407         artist << track->artist();
408         album << track->album();
409         date << track->date();
410         discPerformer << track->tag(TagId::AlbumArtist);
411     }
412 
413     tagGenreEdit->setMultiValue(genre);
414     tagYearEdit->setMultiValue(date);
415     tagArtistEdit->setMultiValue(artist);
416     tagAlbumEdit->setMultiValue(album);
417     tagStartNumEdit->setMultiValue(startNums);
418     tagDiscIdEdit->setMultiValue(discId);
419     codepageCombo->setMultiValue(codePage);
420     tagDiscPerformerEdit->setMultiValue(discPerformer);
421 
422     const Profile &profile = Settings::i()->currentProfile();
423     if (outDirEdit->currentText() != profile.outFileDir())
424         outDirEdit->lineEdit()->setText(profile.outFileDir());
425 
426     if (outPatternEdit->currentText() != profile.outFilePattern())
427         outPatternEdit->lineEdit()->setText(profile.outFilePattern());
428 
429     refreshOutProfileCombo();
430 }
431 
432 /************************************************
433 
434  ************************************************/
refreshOutProfileCombo()435 void MainWindow::refreshOutProfileCombo()
436 {
437     outProfileCombo->blockSignals(true);
438     int n = 0;
439     for (const Profile &p : Settings::i()->profiles()) {
440         if (n < outProfileCombo->count()) {
441             outProfileCombo->setItemText(n, p.name());
442             outProfileCombo->setItemData(n, p.id());
443         }
444         else {
445             outProfileCombo->addItem(p.name(), p.id());
446         }
447         n++;
448     }
449     while (outProfileCombo->count() > n)
450         outProfileCombo->removeItem(outProfileCombo->count() - 1);
451 
452     outProfileCombo->blockSignals(false);
453 
454     n = outProfileCombo->findData(Settings::i()->currentProfile().id());
455     outProfileCombo->setCurrentIndex(qMax(0, n));
456 }
457 
458 /************************************************
459 
460  ************************************************/
setCodePage()461 void MainWindow::setCodePage()
462 {
463     int n = codepageCombo->currentIndex();
464     if (n > -1) {
465         QString codepage = codepageCombo->itemData(n).toString();
466 
467         QList<Disc *> discs = trackView->selectedDiscs();
468         foreach (Disc *disc, discs)
469             disc->setCodecName(codepage);
470 
471         Settings::i()->setValue(Settings::Tags_DefaultCodepage, codepage);
472     }
473 }
474 
475 /************************************************
476 
477  ************************************************/
setTrackTag()478 void MainWindow::setTrackTag()
479 {
480     TagLineEdit *edit = qobject_cast<TagLineEdit *>(sender());
481     if (!edit)
482         return;
483 
484     QList<Track *> tracks = trackView->selectedTracks();
485     foreach (Track *track, tracks) {
486         track->setTag(edit->tagId(), edit->text());
487         trackView->update(*track);
488     }
489 }
490 
491 /************************************************
492  *
493  ************************************************/
setDiscTag()494 void MainWindow::setDiscTag()
495 {
496     TagLineEdit *edit = qobject_cast<TagLineEdit *>(sender());
497     if (!edit)
498         return;
499 
500     QList<Disc *> discs = trackView->selectedDiscs();
501     foreach (Disc *disc, discs) {
502         disc->setDiscTag(edit->tagId(), edit->text());
503         trackView->update(*disc);
504     }
505 }
506 
507 /************************************************
508  *
509  ************************************************/
setDiscTagInt()510 void MainWindow::setDiscTagInt()
511 {
512     TagSpinBox *spinBox = qobject_cast<TagSpinBox *>(sender());
513     if (!spinBox)
514         return;
515 
516     QList<Disc *> discs = trackView->selectedDiscs();
517     foreach (Disc *disc, discs) {
518         disc->setDiscTag(spinBox->tagId(), QString::number(spinBox->value()));
519         trackView->update(*disc);
520     }
521 }
522 
523 /************************************************
524  *
525  ************************************************/
startConvertAll()526 void MainWindow::startConvertAll()
527 {
528     Conv::Converter::Jobs jobs;
529     for (int d = 0; d < project->count(); ++d) {
530         Conv::Converter::Job job;
531         job.disc = project->disc(d);
532 
533         for (int t = 0; t < job.disc->count(); ++t) {
534             job.tracks << job.disc->track(t);
535         }
536 
537         jobs << job;
538     }
539 
540     startConvert(jobs);
541 }
542 
543 /************************************************
544  *
545  ************************************************/
startConvertSelected()546 void MainWindow::startConvertSelected()
547 {
548     Conv::Converter::Jobs jobs;
549     for (Disc *disc : trackView->selectedDiscs()) {
550         Conv::Converter::Job job;
551         job.disc = disc;
552 
553         for (int t = 0; t < disc->count(); ++t) {
554             Track *track = disc->track(t);
555             if (trackView->isSelected(*track)) {
556                 job.tracks << track;
557             }
558         }
559 
560         jobs << job;
561     }
562 
563     startConvert(jobs);
564 }
565 
566 /************************************************
567 
568  ************************************************/
startConvert(const Conv::Converter::Jobs & jobs)569 void MainWindow::startConvert(const Conv::Converter::Jobs &jobs)
570 {
571     if (!Settings::i()->currentProfile().isValid())
572         return;
573 
574     trackView->setFocus();
575 
576     bool ok = true;
577     for (int i = 0; i < project->count(); ++i) {
578         ok = ok && project->disc(i)->canConvert();
579     }
580 
581     if (!ok) {
582         int res = QMessageBox::warning(this,
583                                        windowTitle(),
584                                        tr("Some albums will not be converted, they contain errors.\nDo you want to continue?"),
585                                        QMessageBox::Ok | QMessageBox::Cancel);
586         if (res != QMessageBox::Ok)
587             return;
588     }
589 
590     for (int d = 0; d < project->count(); ++d) {
591         Disc *disc = project->disc(d);
592         for (int t = 0; t < disc->count(); ++t)
593             trackView->model()->trackProgressChanged(*disc->track(t), TrackState::NotRunning, 0);
594     }
595 
596     trackView->setColumnWidth(TrackView::ColumnPercent, 200);
597     mConverter = new Conv::Converter();
598     connect(mConverter, &Conv::Converter::finished,
599             this, &MainWindow::setControlsEnable);
600 
601     connect(mConverter, &Conv::Converter::finished,
602             mConverter, &Conv::Converter::deleteLater);
603 
604     connect(mConverter, &Conv::Converter::trackProgress,
605             trackView->model(), &TrackViewModel::trackProgressChanged);
606 
607     connect(mConverter, &Conv::Converter::error,
608             this, &MainWindow::showErrorMessage);
609 
610     mConverter->start(jobs, Settings::i()->currentProfile());
611     setControlsEnable();
612 }
613 
614 /************************************************
615 
616  ************************************************/
stopConvert()617 void MainWindow::stopConvert()
618 {
619     if (mConverter)
620         mConverter->stop();
621     setControlsEnable();
622 }
623 
624 /************************************************
625 
626  ************************************************/
configure()627 void MainWindow::configure()
628 {
629     auto dlg = PreferencesDialog::createAndShow(nullptr, this);
630     connect(dlg, &PreferencesDialog::finished, this, &MainWindow::refreshEdits, Qt::UniqueConnection);
631 }
632 
633 /************************************************
634 
635  ************************************************/
configureEncoder()636 void MainWindow::configureEncoder()
637 {
638     auto dlg = PreferencesDialog::createAndShow(Settings::i()->currentProfile().id(), this);
639     connect(dlg, &PreferencesDialog::finished, this, &MainWindow::refreshEdits, Qt::UniqueConnection);
640 }
641 
642 /************************************************
643 
644  ************************************************/
downloadInfo()645 void MainWindow::downloadInfo()
646 {
647     QList<Disc *> discs = trackView->selectedDiscs();
648     foreach (Disc *disc, discs) {
649         this->downloadDiscInfo(disc);
650     }
651 }
652 
653 /************************************************
654 
655  ************************************************/
getOpenFileFilter(bool includeAudio,bool includeCue)656 QString MainWindow::getOpenFileFilter(bool includeAudio, bool includeCue)
657 {
658     QStringList flt;
659     QStringList allFlt;
660     QString     fltPattern = tr("%1 files", "OpenFile dialog filter line, like \"WAV files\"") + " (*.%2)";
661 
662     if (includeAudio) {
663         foreach (const InputFormat *format, InputFormat::allFormats()) {
664             allFlt << QString(" *.%1").arg(format->ext());
665             flt << fltPattern.arg(format->name(), format->ext());
666         }
667     }
668 
669     flt.sort();
670 
671     if (includeCue) {
672         allFlt << QString("*.cue");
673         flt.insert(0, fltPattern.arg("CUE", "cue"));
674     }
675 
676     if (allFlt.count() > 1)
677         flt.insert(0, tr("All supported formats", "OpenFile dialog filter line") + " (" + allFlt.join(" ") + ")");
678 
679     flt << tr("All files", "OpenFile dialog filter line like \"All files\"") + " (*)";
680 
681     return flt.join(";;");
682 }
683 
684 /************************************************
685 
686  ************************************************/
openAddFileDialog()687 void MainWindow::openAddFileDialog()
688 {
689     QString     flt       = getOpenFileFilter(true, true);
690     QString     lastDir   = Settings::i()->value(Settings::Misc_LastDir).toString();
691     QStringList fileNames = QFileDialog::getOpenFileNames(this, tr("Add CUE or audio file", "OpenFile dialog title"), lastDir, flt);
692 
693     if (fileNames.isEmpty()) {
694         return;
695     }
696 
697     Settings::i()->setValue(Settings::Misc_LastDir, QFileInfo(fileNames.last()).dir().path());
698 
699     foreach (const QString &fileName, fileNames) {
700         addFileOrDir(fileName);
701     }
702 }
703 
704 /************************************************
705 
706  ************************************************/
setAudioForDisc(Disc * disc,int audioFileNum)707 void MainWindow::setAudioForDisc(Disc *disc, int audioFileNum)
708 {
709     QString flt = getOpenFileFilter(true, false);
710 
711     QString dir;
712     {
713         QStringList audioFiles = disc->audioFilePaths();
714 
715         if (!disc->cueFilePath().isEmpty()) {
716             dir = QFileInfo(disc->cueFilePath()).dir().absolutePath();
717         }
718         else if (audioFileNum < audioFiles.count() && !audioFiles[audioFileNum].isEmpty()) {
719             dir = QFileInfo(audioFiles[audioFileNum]).dir().absolutePath();
720         }
721         else {
722             dir = Settings::i()->value(Settings::Misc_LastDir).toString();
723         }
724     }
725 
726     QString fileName = QFileDialog::getOpenFileName(this, tr("Select audio file", "OpenFile dialog title"), dir, flt);
727 
728     if (fileName.isEmpty())
729         return;
730 
731     InputAudioFile audio(fileName);
732     if (!audio.isValid()) {
733         Messages::error(tr("\"%1\" was not set.", "Error message, %1 is an filename.")
734                                 .arg(QFileInfo(fileName).fileName())
735                         + "<br>" + audio.errorString());
736         return;
737     }
738 
739     disc->setAudioFile(audio, audioFileNum);
740     trackView->update(*disc);
741 }
742 
743 /************************************************
744  *
745  ************************************************/
setCoverImage(Disc * disc)746 void MainWindow::setCoverImage(Disc *disc)
747 {
748     CoverDialog::createAndShow(disc, this);
749 }
750 
751 /************************************************
752  *
753  ************************************************/
downloadDiscInfo(Disc * disc)754 void MainWindow::downloadDiscInfo(Disc *disc)
755 {
756     if (!disc->canDownloadInfo())
757         return;
758 
759     DataProvider *provider = new FreeDbProvider(*disc);
760     connect(provider, &DataProvider::finished,
761             provider, &DataProvider::deleteLater);
762 
763     connect(provider, &DataProvider::finished, provider,
764             [disc, this]() { trackView->downloadFinished(*disc); });
765 
766     connect(provider, &DataProvider::ready, provider,
767             [disc](const QVector<Tracks> data) { disc->addTagSets(data); });
768 
769     provider->start();
770     trackView->downloadStarted(*disc);
771 }
772 
773 /************************************************
774 
775  ************************************************/
addFileOrDir(const QString & fileName)776 void MainWindow::addFileOrDir(const QString &fileName)
777 {
778     bool isFirst   = true;
779     bool showError = false;
780     auto addFile   = [&](const QString &file) {
781         try {
782             QFileInfo fi = QFileInfo(file);
783             DiscList  discs;
784             if (fi.size() > 102400)
785                 discs << project->addAudioFile(file);
786             else
787                 discs << project->addCueFile(file);
788 
789             if (!discs.isEmpty() && isFirst) {
790                 isFirst = false;
791                 this->trackView->selectDisc(discs.first());
792             }
793         }
794 
795         catch (FlaconError &err) {
796             if (showError)
797                 showErrorMessage(err.what());
798         }
799     };
800 
801     QApplication::setOverrideCursor(Qt::WaitCursor);
802     QFileInfo fi = QFileInfo(fileName);
803 
804     if (fi.isDir()) {
805         mScanner = new Scanner;
806         setControlsEnable();
807         showError = false;
808         connect(mScanner, &Scanner::found, addFile);
809         mScanner->start(fi.absoluteFilePath());
810         delete mScanner;
811         mScanner = nullptr;
812         setControlsEnable();
813     }
814     else {
815         showError = true;
816         addFile(fileName);
817     }
818     QApplication::restoreOverrideCursor();
819 }
820 
821 /************************************************
822 
823  ************************************************/
removeDiscs()824 void MainWindow::removeDiscs()
825 {
826     QList<Disc *> discs = trackView->selectedDiscs();
827     if (discs.isEmpty())
828         return;
829 
830     int n = project->indexOf(discs.first());
831     project->removeDisc(&discs);
832 
833     n = qMin(n, project->count() - 1);
834     if (n > -1)
835         trackView->selectDisc(project->disc(n));
836 
837     setControlsEnable();
838 }
839 
840 /************************************************
841 
842  ************************************************/
openScanDialog()843 void MainWindow::openScanDialog()
844 {
845     QString lastDir = Settings::i()->value(Settings::Misc_LastDir).toString();
846     QString dir     = QFileDialog::getExistingDirectory(this, tr("Select directory"), lastDir);
847 
848     if (!dir.isEmpty()) {
849         Settings::i()->setValue(Settings::Misc_LastDir, dir);
850         addFileOrDir(dir);
851     }
852 }
853 
854 /************************************************
855 
856  ************************************************/
openAboutDialog()857 void MainWindow::openAboutDialog()
858 {
859     AboutDialog dlg;
860     dlg.exec();
861 }
862 
863 /************************************************
864 
865  ************************************************/
checkUpdates()866 void MainWindow::checkUpdates()
867 {
868 #ifdef MAC_UPDATER
869     Updater::sharedUpdater().checkForUpdates("io.github.flacon");
870 #endif
871 }
872 
873 /************************************************
874  *
875  ************************************************/
fillAudioMenu(Disc * disc,QMenu & menu)876 void MainWindow::fillAudioMenu(Disc *disc, QMenu &menu)
877 {
878     QAction *act;
879     if (disc->audioFiles().count() == 1) {
880         act = new QAction(tr("Select another audio file…", "context menu"), &menu);
881         connect(act, &QAction::triggered, [this, disc]() { this->setAudioForDisc(disc, 0); });
882         menu.addAction(act);
883     }
884     else {
885         int n = 0;
886         for (TrackPtrList &l : disc->tracksByFileTag()) {
887             QString msg;
888             if (l.count() == 1) {
889                 msg = tr("Select another audio file for %1 track…", "context menu. Placeholders are track number")
890                               .arg(l.first()->trackNum());
891             }
892             else {
893                 msg = tr("Select another audio file for tracks %1 to %2…", "context menu. Placeholders are track numbers")
894                               .arg(l.first()->trackNum())
895                               .arg(l.last()->trackNum());
896             }
897 
898             act = new QAction(msg, &menu);
899             connect(act, &QAction::triggered, [this, disc, n]() { this->setAudioForDisc(disc, n); });
900             menu.addAction(act);
901 
902             n++;
903         }
904     }
905 }
906 
907 /************************************************
908  *
909  ************************************************/
trackViewMenu(const QPoint & pos)910 void MainWindow::trackViewMenu(const QPoint &pos)
911 {
912     QModelIndex index = trackView->indexAt(pos);
913     if (!index.isValid())
914         return;
915 
916     Disc *disc = trackView->model()->discByIndex(index);
917     if (!disc)
918         return;
919 
920     QMenu    menu;
921     QAction *act = new QAction(tr("Edit tags…", "context menu"), &menu);
922     connect(act, &QAction::triggered, this, &MainWindow::openEditTagsDialog);
923     menu.addAction(act);
924 
925     menu.addSeparator();
926     fillAudioMenu(disc, menu);
927 
928     act = new QAction(tr("Select another CUE file…", "context menu"), &menu);
929     connect(act, &QAction::triggered, [this, disc]() { this->setCueForDisc(disc); });
930     menu.addAction(act);
931 
932     act = new QAction(tr("Get data from CDDB", "context menu"), &menu);
933     act->setEnabled(disc->canDownloadInfo());
934     connect(act, &QAction::triggered, [this, disc]() { this->downloadDiscInfo(disc); });
935     menu.addAction(act);
936 
937     menu.exec(trackView->viewport()->mapToGlobal(pos));
938 }
939 
940 /************************************************
941  *
942  ************************************************/
showDiskAudioFileMenu(Disc * disc,const QPoint & pos)943 void MainWindow::showDiskAudioFileMenu(Disc *disc, const QPoint &pos)
944 {
945     QMenu menu;
946     fillAudioMenu(disc, menu);
947     menu.exec(trackView->viewport()->mapToGlobal(pos));
948 }
949 
950 /************************************************
951  *
952  ************************************************/
openEditTagsDialog()953 void MainWindow::openEditTagsDialog()
954 {
955     TagEditor editor(trackView->selectedTracks(), trackView->selectedDiscs(), this);
956     editor.exec();
957     refreshEdits();
958 }
959 
960 /************************************************
961 
962  ************************************************/
keyPressEvent(QKeyEvent * event)963 void MainWindow::keyPressEvent(QKeyEvent *event)
964 {
965     if (event->key() == Qt::Key_Escape) {
966         if (mScanner)
967             mScanner->stop();
968     }
969 }
970 
971 /************************************************
972 
973  ************************************************/
event(QEvent * event)974 bool MainWindow::event(QEvent *event)
975 {
976     switch (event->type()) {
977 #ifdef Q_OS_MAC
978         case QEvent::WindowActivate:
979             toolBar->setEnabled(true);
980             break;
981 
982         case QEvent::WindowDeactivate:
983             toolBar->setEnabled(false);
984             break;
985 #endif
986         default:
987             break;
988     }
989     return QMainWindow::event(event);
990 }
991 
992 /************************************************
993 
994  ************************************************/
setStartTrackNum()995 void MainWindow::setStartTrackNum()
996 {
997     if (!tagStartNumEdit->isModified())
998         return;
999 
1000     int           value = tagStartNumEdit->value();
1001     QList<Disc *> discs = trackView->selectedDiscs();
1002     foreach (Disc *disc, discs) {
1003         disc->setStartTrackNum(value);
1004     }
1005 }
1006 
1007 /************************************************
1008 
1009  ************************************************/
initActions()1010 void MainWindow::initActions()
1011 {
1012     actionAddDisc->setIcon(Icon("add-disk"));
1013     connect(actionAddDisc, &QAction::triggered, this, &MainWindow::openAddFileDialog);
1014 
1015     actionRemoveDisc->setIcon(Icon("remove-disk"));
1016     connect(actionRemoveDisc, &QAction::triggered, this, &MainWindow::removeDiscs);
1017 
1018     actionAddFolder->setIcon(Icon("scan"));
1019     connect(actionAddFolder, &QAction::triggered, this, &MainWindow::openScanDialog);
1020 
1021     actionDownloadTrackInfo->setIcon(Icon("download-info"));
1022     connect(actionDownloadTrackInfo, &QAction::triggered, this, &MainWindow::downloadInfo);
1023 
1024     actionStartConvert->setIcon(Icon("start-convert"));
1025     connect(actionStartConvert, &QAction::triggered, this, &MainWindow::startConvertAll);
1026 
1027     actionStartConvertSelected->setIcon(Icon("start-convert"));
1028     connect(actionStartConvertSelected, &QAction::triggered, this, &MainWindow::startConvertSelected);
1029 
1030     actionAbortConvert->setIcon(Icon("abort-convert"));
1031     connect(actionAbortConvert, &QAction::triggered, this, &MainWindow::stopConvert);
1032 
1033     actionConfigure->setIcon(Icon("configure"));
1034     connect(actionConfigure, &QAction::triggered, this, &MainWindow::configure);
1035     actionConfigure->setMenuRole(QAction::PreferencesRole);
1036 
1037     actionConfigureEncoder->setIcon(actionConfigure->icon());
1038     connect(actionConfigureEncoder, &QAction::triggered, this, &MainWindow::configureEncoder);
1039 
1040     connect(actionAbout, &QAction::triggered, this, &MainWindow::openAboutDialog);
1041     actionAbout->setMenuRole(QAction::AboutRole);
1042 
1043     Controls::arangeTollBarButtonsWidth(toolBar);
1044 
1045 #ifdef MAC_UPDATER
1046     actionUpdates->setVisible(true);
1047     actionUpdates->setMenuRole(QAction::ApplicationSpecificRole);
1048 
1049     connect(actionUpdates, &QAction::triggered,
1050             this, &MainWindow::checkUpdates);
1051 #else
1052     actionUpdates->setVisible(false);
1053 #endif
1054 }
1055 
1056 /************************************************
1057   Load settings
1058  ************************************************/
loadSettings()1059 void MainWindow::loadSettings()
1060 {
1061     // MainWindow geometry
1062     int width  = Settings::i()->value(Settings::MainWindow_Width, QVariant(987)).toInt();
1063     int height = Settings::i()->value(Settings::MainWindow_Height, QVariant(450)).toInt();
1064     this->resize(width, height);
1065 
1066     splitter->restoreState(Settings::i()->value("MainWindow/Splitter").toByteArray());
1067     trackView->header()->restoreState(Settings::i()->value("MainWindow/TrackView").toByteArray());
1068 
1069     outDirEdit->setHistory(Settings::i()->value(Settings::OutFiles_DirectoryHistory).toStringList());
1070     outPatternEdit->setHistory(Settings::i()->value(Settings::OutFiles_PatternHistory).toStringList());
1071 }
1072 
1073 /************************************************
1074   Write settings
1075  ************************************************/
saveSettings()1076 void MainWindow::saveSettings()
1077 {
1078     Settings::i()->setValue("MainWindow/Width", QVariant(size().width()));
1079     Settings::i()->setValue("MainWindow/Height", QVariant(size().height()));
1080     Settings::i()->setValue("MainWindow/Splitter", QVariant(splitter->saveState()));
1081     Settings::i()->setValue("MainWindow/TrackView", QVariant(trackView->header()->saveState()));
1082 
1083     Settings::i()->setValue(Settings::OutFiles_DirectoryHistory, outDirEdit->history());
1084     Settings::i()->setValue(Settings::OutFiles_PatternHistory, outPatternEdit->history());
1085 }
1086 
1087 /************************************************
1088  *
1089  ************************************************/
loadMainIcon()1090 QIcon MainWindow::loadMainIcon()
1091 {
1092     if (QIcon::themeName() == "hicolor") {
1093         QStringList failback;
1094         failback << "oxygen";
1095         failback << "Tango";
1096         failback << "Prudence-icon";
1097         failback << "Humanity";
1098         failback << "elementary";
1099         failback << "gnome";
1100 
1101         QDir usrDir("/usr/share/icons/");
1102         QDir usrLocalDir("/usr/local/share/icons/");
1103         foreach (QString s, failback) {
1104             if (usrDir.exists(s) || usrLocalDir.exists(s)) {
1105                 QIcon::setThemeName(s);
1106                 break;
1107             }
1108         }
1109     }
1110 
1111     return QIcon::fromTheme("flacon", Icon("mainicon"));
1112 }
1113 
1114 /************************************************
1115  *
1116  ************************************************/
showErrorMessage(const QString & message)1117 void MainWindow::showErrorMessage(const QString &message)
1118 {
1119     const QString name = "errorMessage";
1120     ErrorBox     *box  = this->findChild<ErrorBox *>(name);
1121     if (!box) {
1122         box = new ErrorBox(this);
1123         box->setObjectName(name);
1124         box->setWindowTitle(QObject::tr("Flacon", "Error"));
1125         box->setAttribute(Qt::WA_DeleteOnClose, true);
1126     }
1127 
1128     QString msg = message;
1129     msg.replace('\n', "<br>\n");
1130     box->addMessage(msg);
1131     box->open();
1132 }
1133