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