1 /* ============================================================
2  *
3  * This file is a part of digiKam project
4  * https://www.digikam.org
5  *
6  * Date        : 2008-09-14
7  * Description : a presentation tool.
8  *
9  * Copyright (C) 2008-2009 by Valerio Fuoglio <valerio dot fuoglio at gmail dot com>
10  * Copyright (C) 2012-2021 by Gilles Caulier <caulier dot gilles at gmail dot com>
11  *
12  * This program is free software; you can redistribute it
13  * and/or modify it under the terms of the GNU General
14  * Public License as published by the Free Software Foundation;
15  * either version 2, or (at your option) any later version.
16  *
17  * This program is distributed in the hope that it will be useful,
18  * but WITHOUT ANY WARRANTY; without even the implied warranty of
19  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20  * GNU General Public License for more details.
21  *
22  * ============================================================ */
23 
24 #include "presentation_audiopage.h"
25 
26 // Qt includes
27 
28 #include <QPointer>
29 #include <QTime>
30 #include <QIcon>
31 #include <QMessageBox>
32 #include <QDialogButtonBox>
33 
34 // Local includes
35 
36 #include "presentationaudiowidget.h"
37 #include "presentation_mainpage.h"
38 #include "presentationcontainer.h"
39 #include "digikam_debug.h"
40 #include "dfiledialog.h"
41 
42 using namespace Digikam;
43 
44 namespace DigikamGenericPresentationPlugin
45 {
46 
SoundtrackPreview(QWidget * const parent,const QList<QUrl> & urls,PresentationContainer * const sharedData)47 SoundtrackPreview::SoundtrackPreview(QWidget* const parent,
48                                      const QList<QUrl>& urls,
49                                      PresentationContainer* const sharedData)
50     : QDialog(parent)
51 {
52     setModal(true);
53     setWindowTitle(i18n("Soundtrack preview"));
54 
55     m_playbackWidget                  = new PresentationAudioWidget(this, urls, sharedData);
56     QDialogButtonBox* const buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, this);
57 
58     connect(buttonBox, &QDialogButtonBox::rejected,
59             this, &QDialog::reject);
60 
61     QVBoxLayout* const layout = new QVBoxLayout(this);
62     layout->addWidget(m_playbackWidget);
63     layout->addWidget(buttonBox);
64     setLayout(layout);
65 }
66 
~SoundtrackPreview()67 SoundtrackPreview::~SoundtrackPreview()
68 {
69 }
70 
71 // ------------------------------------------------------------------------------------
72 
73 class Q_DECL_HIDDEN PresentationAudioPage::Private
74 {
75 public:
76 
Private()77     explicit Private()
78       : sharedData(nullptr),
79         tracksTime(nullptr),
80         soundItems(nullptr),
81         timeMutex (nullptr)
82     {
83     }
84 
85     QList<QUrl>                             urlList;
86     PresentationContainer*                  sharedData;
87     QTime                                   totalTime;
88     QTime                                   imageTime;
89     QMap<QUrl, QTime>*                      tracksTime;
90     QMap<QUrl, PresentationAudioListItem*>* soundItems;
91     QMutex*                                 timeMutex;
92 };
93 
PresentationAudioPage(QWidget * const parent,PresentationContainer * const sharedData)94 PresentationAudioPage::PresentationAudioPage(QWidget* const parent,
95                                              PresentationContainer* const sharedData)
96     : QWidget(parent),
97       d      (new Private)
98 {
99     setupUi(this);
100 
101     d->sharedData = sharedData;
102     d->totalTime  = QTime(0, 0, 0);
103     d->imageTime  = QTime(0, 0, 0);
104     d->tracksTime = new QMap<QUrl, QTime>();
105     d->soundItems = new QMap<QUrl, PresentationAudioListItem*>();
106     d->timeMutex  = new QMutex();
107 
108     m_soundtrackTimeLabel->setText(d->totalTime.toString());
109     m_previewButton->setEnabled(false);
110 
111     m_rememberSoundtrack->setToolTip(i18n("If set, the soundtrack for the current album "
112                                           "will be saved and restored automatically on the next startup."));
113 
114     // --------------------------------------------------------
115 
116     m_SoundFilesButtonUp->setIcon(QIcon::fromTheme(QLatin1String("go-up")));
117     m_SoundFilesButtonDown->setIcon(QIcon::fromTheme(QLatin1String("go-down")));
118     m_SoundFilesButtonAdd->setIcon(QIcon::fromTheme(QLatin1String("list-add")));
119     m_SoundFilesButtonDelete->setIcon(QIcon::fromTheme(QLatin1String("list-remove")));
120     m_SoundFilesButtonLoad->setIcon(QIcon::fromTheme(QLatin1String("document-open")));
121     m_SoundFilesButtonSave->setIcon(QIcon::fromTheme(QLatin1String("document-save")));
122     m_SoundFilesButtonReset->setIcon(QIcon::fromTheme(QLatin1String("edit-clear")));
123 
124     m_SoundFilesButtonUp->setText(QString());
125     m_SoundFilesButtonDown->setText(QString());
126     m_SoundFilesButtonAdd->setText(QString());
127     m_SoundFilesButtonDelete->setText(QString());
128     m_SoundFilesButtonLoad->setText(QString());
129     m_SoundFilesButtonSave->setText(QString());
130     m_SoundFilesButtonReset->setText(QString());
131 
132     m_SoundFilesButtonUp->setToolTip(i18n("Move the selected track up in the playlist."));
133     m_SoundFilesButtonDown->setToolTip(i18n("Move the selected track down in the playlist."));
134     m_SoundFilesButtonAdd->setToolTip(i18n("Add new tracks to the playlist."));
135     m_SoundFilesButtonDelete->setToolTip(i18n("Delete the selected track from the playlist."));
136     m_SoundFilesButtonLoad->setToolTip(i18n("Load playlist from a file."));
137     m_SoundFilesButtonSave->setToolTip(i18n("Save playlist to a file."));
138     m_SoundFilesButtonReset->setToolTip(i18n("Clear the playlist."));
139 
140     // --------------------------------------------------------
141 
142     connect(m_SoundFilesListBox, SIGNAL(currentRowChanged(int)),
143             this, SLOT(slotSoundFilesSelected(int)));
144 
145     connect(m_SoundFilesListBox, SIGNAL(signalAddedDropItems(QList<QUrl>)),
146             this, SLOT(slotAddDropItems(QList<QUrl>)));
147 
148     connect(m_SoundFilesButtonAdd, SIGNAL(clicked()),
149             this, SLOT(slotSoundFilesButtonAdd()));
150 
151     connect(m_SoundFilesButtonDelete, SIGNAL(clicked()),
152             this, SLOT(slotSoundFilesButtonDelete()));
153 
154     connect(m_SoundFilesButtonUp, SIGNAL(clicked()),
155             this, SLOT(slotSoundFilesButtonUp()));
156 
157     connect(m_SoundFilesButtonDown, SIGNAL(clicked()),
158             this, SLOT(slotSoundFilesButtonDown()));
159 
160     connect(m_SoundFilesButtonLoad, SIGNAL(clicked()),
161             this, SLOT(slotSoundFilesButtonLoad()));
162 
163     connect(m_SoundFilesButtonSave, SIGNAL(clicked()),
164             this, SLOT(slotSoundFilesButtonSave()));
165 
166     connect(m_SoundFilesButtonReset, SIGNAL(clicked()),
167             this, SLOT(slotSoundFilesButtonReset()));
168 
169     connect(m_previewButton, SIGNAL(clicked()),
170             this, SLOT(slotPreviewButtonClicked()));
171 
172     connect(d->sharedData->mainPage, SIGNAL(signalTotalTimeChanged(QTime)),
173             this, SLOT(slotImageTotalTimeChanged(QTime)));
174 }
175 
~PresentationAudioPage()176 PresentationAudioPage::~PresentationAudioPage()
177 {
178     delete d->tracksTime;
179     delete d->soundItems;
180     delete d->timeMutex;
181     delete d;
182 }
183 
readSettings()184 void PresentationAudioPage::readSettings()
185 {
186     m_rememberSoundtrack->setChecked(d->sharedData->soundtrackRememberPlaylist);
187     m_loopCheckBox->setChecked(d->sharedData->soundtrackLoop);
188     m_playCheckBox->setChecked(d->sharedData->soundtrackPlay);
189 
190     connect(d->sharedData->mainPage, SIGNAL(signalTotalTimeChanged(QTime)),
191             this, SLOT(slotImageTotalTimeChanged(QTime)));
192 
193     // if tracks are already set in d->sharedData, add them now
194 
195     if (!d->sharedData->soundtrackUrls.isEmpty())
196     {
197         addItems(d->sharedData->soundtrackUrls);
198     }
199 
200     updateFileList();
201     updateTracksNumber();
202 }
203 
saveSettings()204 void PresentationAudioPage::saveSettings()
205 {
206     d->sharedData->soundtrackRememberPlaylist = m_rememberSoundtrack->isChecked();
207     d->sharedData->soundtrackLoop             = m_loopCheckBox->isChecked();
208     d->sharedData->soundtrackPlay             = m_playCheckBox->isChecked();
209     d->sharedData->soundtrackUrls             = d->urlList;
210 }
211 
addItems(const QList<QUrl> & fileList)212 void PresentationAudioPage::addItems(const QList<QUrl>& fileList)
213 {
214     if (fileList.isEmpty())
215     {
216         return;
217     }
218 
219     QList<QUrl> Files = fileList;
220 
221     for (QList<QUrl>::ConstIterator it = Files.constBegin() ; it != Files.constEnd() ; ++it)
222     {
223         QUrl currentFile                      = *it;
224         d->sharedData->soundtrackPath         = currentFile;
225         PresentationAudioListItem* const item = new PresentationAudioListItem(m_SoundFilesListBox, currentFile);
226         item->setName(currentFile.fileName());
227         m_SoundFilesListBox->insertItem(m_SoundFilesListBox->count() - 1, item);
228 
229         d->soundItems->insert(currentFile, item);
230 
231         connect(d->soundItems->value(currentFile), SIGNAL(signalTotalTimeReady(QUrl,QTime)),
232                 this, SLOT(slotAddNewTime(QUrl,QTime)));
233 
234         d->urlList.append(currentFile);
235     }
236 
237     m_SoundFilesListBox->setCurrentItem(m_SoundFilesListBox->item(m_SoundFilesListBox->count() - 1)) ;
238 
239     slotSoundFilesSelected(m_SoundFilesListBox->currentRow());
240     m_SoundFilesListBox->scrollToItem(m_SoundFilesListBox->currentItem());
241     m_previewButton->setEnabled(true);
242 }
243 
updateTracksNumber()244 void PresentationAudioPage::updateTracksNumber()
245 {
246     QTime displayTime(0, 0, 0);
247     int number = m_SoundFilesListBox->count();
248 
249     if (number > 0)
250     {
251         displayTime = displayTime.addMSecs(1000 * (number - 1));
252 
253         for (QMap<QUrl, QTime>::iterator it = d->tracksTime->begin() ; it != d->tracksTime->end() ; ++it)
254         {
255             int hours = it.value().hour()   + displayTime.hour();
256             int mins  = it.value().minute() + displayTime.minute();
257             int secs  = it.value().second() + displayTime.second();
258 
259             /*
260              * QTime doesn't get a overflow value in input. They need
261              * to be cut down to size.
262              */
263 
264             mins        = mins + (int)(secs / 60);
265             secs        = secs % 60;
266             hours       = hours + (int)(mins / 60);
267             displayTime = QTime(hours, mins, secs);
268         }
269     }
270 
271     m_timeLabel->setText(i18ncp("number of tracks and running time", "1 track [%2]", "%1 tracks [%2]", number, displayTime.toString()));
272 
273     m_soundtrackTimeLabel->setText(displayTime.toString());
274 
275     d->totalTime = displayTime;
276 
277     compareTimes();
278 }
279 
updateFileList()280 void PresentationAudioPage::updateFileList()
281 {
282     d->urlList = m_SoundFilesListBox->fileUrls();
283 
284     m_SoundFilesButtonUp->setEnabled(!d->urlList.isEmpty());
285     m_SoundFilesButtonDown->setEnabled(!d->urlList.isEmpty());
286     m_SoundFilesButtonDelete->setEnabled(!d->urlList.isEmpty());
287     m_SoundFilesButtonSave->setEnabled(!d->urlList.isEmpty());
288     m_SoundFilesButtonReset->setEnabled(!d->urlList.isEmpty());
289 
290     d->sharedData->soundtrackPlayListNeedsUpdate = true;
291 }
292 
compareTimes()293 void PresentationAudioPage::compareTimes()
294 {
295     QFont statusBarFont = m_statusBarLabel->font();
296 
297     if (d->imageTime > d->totalTime)
298     {
299         m_statusBarLabel->setText(i18n("Slide time is greater than soundtrack time. Suggestion: add more sound files."));
300 
301 
302         QPalette paletteStatusBar = m_statusBarLabel->palette();
303         paletteStatusBar.setColor(QPalette::WindowText, Qt::red);
304         m_statusBarLabel->setPalette(paletteStatusBar);
305 
306         QPalette paletteTimeLabel = m_soundtrackTimeLabel->palette();
307         paletteTimeLabel.setColor(QPalette::WindowText, Qt::red);
308         m_soundtrackTimeLabel->setPalette(paletteTimeLabel);
309 
310         statusBarFont.setItalic(true);
311     }
312     else
313     {
314         m_statusBarLabel->setText(QLatin1String(""));
315 
316         QPalette paletteStatusBar = m_statusBarLabel->palette();
317         paletteStatusBar.setColor(QPalette::WindowText, Qt::red);
318         m_statusBarLabel->setPalette(paletteStatusBar);
319 
320         QPalette paletteTimeLabel = m_soundtrackTimeLabel->palette();
321 
322         if (d->imageTime < d->totalTime)
323         {
324             paletteTimeLabel.setColor(QPalette::WindowText, Qt::black);
325         }
326         else
327         {
328             paletteTimeLabel.setColor(QPalette::WindowText, Qt::green);
329         }
330 
331         m_soundtrackTimeLabel->setPalette(paletteTimeLabel);
332 
333         statusBarFont.setItalic(false);
334     }
335 
336     m_statusBarLabel->setFont(statusBarFont);
337 }
338 
slotAddNewTime(const QUrl & url,const QTime & trackTime)339 void PresentationAudioPage::slotAddNewTime(const QUrl& url, const QTime& trackTime)
340 {
341     d->timeMutex->lock();
342     d->tracksTime->insert(url, trackTime);
343     updateTracksNumber();
344     d->timeMutex->unlock();
345 }
346 
slotSoundFilesSelected(int row)347 void PresentationAudioPage::slotSoundFilesSelected( int row )
348 {
349     QListWidgetItem* const item = m_SoundFilesListBox->item(row);
350 
351     if (!item || (m_SoundFilesListBox->count() == 0))
352     {
353         return;
354     }
355 }
356 
slotAddDropItems(const QList<QUrl> & filesUrl)357 void PresentationAudioPage::slotAddDropItems(const QList<QUrl>& filesUrl)
358 {
359     if (!filesUrl.isEmpty())
360     {
361         addItems(filesUrl);
362         updateFileList();
363     }
364 }
365 
slotSoundFilesButtonAdd()366 void PresentationAudioPage::slotSoundFilesButtonAdd()
367 {
368     QPointer<DFileDialog> dlg = new DFileDialog(this,
369                                                 i18n("Select sound files"),
370                                                 d->sharedData->soundtrackPath.adjusted(QUrl::RemoveFilename).toLocalFile());
371 
372     QStringList atm;
373     atm << QLatin1String("audio/mp3");
374     atm << QLatin1String("audio/wav");
375     atm << QLatin1String("audio/ogg");
376     atm << QLatin1String("audio/flac");
377     dlg->setMimeTypeFilters(atm);
378     dlg->setAcceptMode(QFileDialog::AcceptOpen);
379     dlg->setFileMode(QFileDialog::ExistingFiles);
380 
381     if (dlg->exec() == QDialog::Accepted)
382     {
383         addItems(dlg->selectedUrls());
384         updateFileList();
385     }
386 
387     delete dlg;
388 }
389 
slotSoundFilesButtonDelete()390 void PresentationAudioPage::slotSoundFilesButtonDelete()
391 {
392     int index = m_SoundFilesListBox->currentRow();
393 
394     if (index < 0)
395     {
396         return;
397     }
398 
399     PresentationAudioListItem* const pitem = static_cast<PresentationAudioListItem*>(m_SoundFilesListBox->takeItem(index));
400     d->urlList.removeAll(pitem->url());
401     d->soundItems->remove(pitem->url());
402     d->timeMutex->lock();
403     d->tracksTime->remove(pitem->url());
404     updateTracksNumber();
405     d->timeMutex->unlock();
406     delete pitem;
407     slotSoundFilesSelected(m_SoundFilesListBox->currentRow());
408 
409     if (m_SoundFilesListBox->count() == 0)
410     {
411         m_previewButton->setEnabled(false);
412     }
413 
414     updateFileList();
415 }
416 
slotSoundFilesButtonUp()417 void PresentationAudioPage::slotSoundFilesButtonUp()
418 {
419     int cpt = 0;
420 
421     for (int i = 0 ; i < m_SoundFilesListBox->count() ; ++i)
422     {
423         if (m_SoundFilesListBox->currentRow() == i)
424         {
425             ++cpt;
426         }
427     }
428 
429     if (cpt == 0)
430     {
431         return;
432     }
433 
434     if (cpt > 1)
435     {
436         QMessageBox::critical(this, QString(), i18n("You can only move image files up one at a time."));
437         return;
438     }
439 
440     unsigned int index = m_SoundFilesListBox->currentRow();
441 
442     if (index == 0)
443     {
444         return;
445     }
446 
447     PresentationAudioListItem* const pitem = static_cast<PresentationAudioListItem*>(m_SoundFilesListBox->takeItem(index));
448 
449     m_SoundFilesListBox->insertItem(index - 1, pitem);
450     m_SoundFilesListBox->setCurrentItem(pitem);
451 
452     updateFileList();
453 }
454 
slotSoundFilesButtonDown()455 void PresentationAudioPage::slotSoundFilesButtonDown()
456 {
457     int cpt = 0;
458 
459     for (int i = 0 ; i < m_SoundFilesListBox->count() ; ++i)
460     {
461         if (m_SoundFilesListBox->currentRow() == i)
462         {
463             ++cpt;
464         }
465     }
466 
467     if (cpt == 0)
468     {
469         return;
470     }
471 
472     if (cpt > 1)
473     {
474         QMessageBox::critical(this, QString(), i18n("You can only move files down one at a time."));
475         return;
476     }
477 
478     int index = m_SoundFilesListBox->currentRow();
479 
480     if (index == m_SoundFilesListBox->count())
481     {
482         return;
483     }
484 
485     PresentationAudioListItem* const pitem = static_cast<PresentationAudioListItem*>(m_SoundFilesListBox->takeItem(index));
486 
487     m_SoundFilesListBox->insertItem(index + 1, pitem);
488     m_SoundFilesListBox->setCurrentItem(pitem);
489 
490     updateFileList();
491 }
492 
slotSoundFilesButtonLoad()493 void PresentationAudioPage::slotSoundFilesButtonLoad()
494 {
495     QPointer<DFileDialog> dlg = new DFileDialog(this, i18n("Load playlist"),
496                                                 QString(), i18n("Playlist (*.m3u)"));
497     dlg->setAcceptMode(QFileDialog::AcceptOpen);
498     dlg->setFileMode(QFileDialog::ExistingFile);
499 
500     if (dlg->exec() != QDialog::Accepted)
501     {
502         delete dlg;
503 
504         return;
505     }
506 
507     QString filename = dlg->selectedFiles().first();
508 
509     if (!filename.isEmpty())
510     {
511         QFile file(filename);
512 
513         if (file.open(QIODevice::ReadOnly | QIODevice::Text))
514         {
515             QTextStream in(&file);
516             QList<QUrl> playlistFiles;
517 
518             while (!in.atEnd())
519             {
520                 QString line = in.readLine();
521 
522                 // we ignore the extended information of the m3u playlist file
523 
524                 if (line.startsWith(QLatin1Char('#')) || line.isEmpty())
525                 {
526                     continue;
527                 }
528 
529                 QUrl fUrl = QUrl::fromLocalFile(line);
530 
531                 if (fUrl.isValid() && fUrl.isLocalFile())
532                 {
533                     playlistFiles << fUrl;
534                 }
535             }
536 
537             file.close();
538 
539             if (!playlistFiles.isEmpty())
540             {
541                 m_SoundFilesListBox->clear();
542                 addItems(playlistFiles);
543                 updateFileList();
544             }
545         }
546     }
547 
548     delete dlg;
549 }
550 
slotSoundFilesButtonSave()551 void PresentationAudioPage::slotSoundFilesButtonSave()
552 {
553     QPointer<DFileDialog> dlg = new DFileDialog(this, i18n("Save playlist"),
554                                                 QString(), i18n("Playlist (*.m3u)"));
555     dlg->setAcceptMode(QFileDialog::AcceptSave);
556     dlg->setFileMode(QFileDialog::AnyFile);
557 
558     if (dlg->exec() != QDialog::Accepted)
559     {
560         delete dlg;
561 
562         return;
563     }
564 
565     QString filename = dlg->selectedFiles().first();
566 
567     if (!filename.isEmpty())
568     {
569         QFile file(filename);
570 
571         if (file.open(QIODevice::WriteOnly | QIODevice::Text))
572         {
573             QTextStream out(&file);
574             QList<QUrl> playlistFiles = m_SoundFilesListBox->fileUrls();
575 
576             for (int i = 0 ; i < playlistFiles.count() ; ++i)
577             {
578                 QUrl fUrl(playlistFiles.at(i));
579 
580                 if (fUrl.isValid() && fUrl.isLocalFile())
581                 {
582                     out << fUrl.toLocalFile() << endl;
583                 }
584             }
585 
586             file.close();
587         }
588     }
589 
590     delete dlg;
591 }
592 
slotSoundFilesButtonReset()593 void PresentationAudioPage::slotSoundFilesButtonReset()
594 {
595     m_SoundFilesListBox->clear();
596     updateFileList();
597 }
598 
slotPreviewButtonClicked()599 void PresentationAudioPage::slotPreviewButtonClicked()
600 {
601     QList<QUrl> urlList;
602 
603     for (int i = 0 ; i < m_SoundFilesListBox->count() ; ++i)
604     {
605         PresentationAudioListItem* const pitem = dynamic_cast<PresentationAudioListItem*>(m_SoundFilesListBox->item(i));
606 
607         if (pitem)
608         {
609             QString path = pitem->url().toLocalFile();
610 
611             if (!QFile::exists(path))
612             {
613                 QMessageBox::critical(this, QString(), i18n("Cannot access file \"%1\". Please check the path is correct.", path));
614                 return;
615             }
616 
617             urlList << pitem->url();
618         }
619     }
620 
621     if (urlList.isEmpty())
622     {
623         QMessageBox::critical(this, QString(), i18n("Cannot create a preview of an empty file list."));
624         return;
625     }
626 
627     // Update PresentationContainer from interface
628 
629     saveSettings();
630 
631     qCDebug(DIGIKAM_DPLUGIN_GENERIC_LOG) << "Tracks : " << urlList;
632 
633     QPointer<SoundtrackPreview> preview = new SoundtrackPreview(this, urlList, d->sharedData);
634     preview->exec();
635 
636     delete preview;
637 }
638 
slotImageTotalTimeChanged(const QTime & imageTotalTime)639 void PresentationAudioPage::slotImageTotalTimeChanged(const QTime& imageTotalTime)
640 {
641     d->imageTime = imageTotalTime;
642     m_slideTimeLabel->setText(imageTotalTime.toString());
643     compareTimes();
644 }
645 
646 } // namespace DigikamGenericPresentationPlugin
647