1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2
3 /*
4 Rosegarden
5 A MIDI and audio sequencer and musical notation editor.
6 Copyright 2000-2021 the Rosegarden development team.
7
8 Other copyrights also apply to some parts of this work. Please
9 see the AUTHORS file and individual file headers for details.
10
11 This program is free software; you can redistribute it and/or
12 modify it under the terms of the GNU General Public License as
13 published by the Free Software Foundation; either version 2 of the
14 License, or (at your option) any later version. See the file
15 COPYING included with this distribution for more information.
16 */
17
18
19 #define RG_MODULE_STRING "[AudioManagerDialog]"
20
21 #include "AudioManagerDialog.h"
22
23 #include "base/Event.h"
24 #include "misc/Debug.h"
25 #include "misc/Strings.h"
26 #include "AudioPlayingDialog.h"
27 #include "base/Composition.h"
28 #include "base/Exception.h"
29 #include "base/Instrument.h"
30 #include "base/MidiProgram.h"
31 #include "base/NotationTypes.h"
32 #include "base/RealTime.h"
33 #include "base/Segment.h"
34 #include "base/Selection.h"
35 #include "base/Studio.h"
36 #include "base/Track.h"
37 #include "document/CommandHistory.h"
38 #include "document/RosegardenDocument.h"
39 #include "misc/ConfigGroups.h"
40 #include "gui/application/RosegardenMainWindow.h"
41 #include "gui/application/RosegardenMainViewWidget.h"
42 #include "sequencer/RosegardenSequencer.h"
43 #include "gui/widgets/AudioListItem.h"
44 #include "gui/widgets/AudioListView.h"
45 #include "gui/widgets/TmpStatusMsg.h"
46 #include "gui/widgets/LineEdit.h"
47 #include "gui/widgets/InputDialog.h"
48 #include "gui/widgets/WarningGroupBox.h"
49 #include "gui/general/IconLoader.h"
50 #include "gui/dialogs/AboutDialog.h"
51 #include "sound/AudioFile.h"
52 #include "sound/AudioFileManager.h"
53 #include "sound/WAVAudioFile.h"
54 #include "UnusedAudioSelectionDialog.h"
55 #include "document/Command.h"
56 #include "gui/widgets/FileDialog.h"
57
58 #include <QApplication>
59 #include <QMainWindow>
60 #include <QTreeWidget>
61 #include <QTreeWidgetItem>
62 #include <QTreeWidgetItemIterator>
63 #include <QMessageBox>
64 #include <QAction>
65 #include <QByteArray>
66 #include <QDataStream>
67 #include <QDialog>
68 #include <QFile>
69 #include <QFileInfo>
70 #include <QIcon>
71 #include <QLabel>
72 #include <QTreeWidget>
73 #include <QPainter>
74 #include <QPixmap>
75 #include <QString>
76 #include <QStringList>
77 #include <QTimer>
78 #include <QWidget>
79 #include <QVBoxLayout>
80 #include <QUrl>
81 #include <QShortcut>
82 #include <QKeySequence>
83 #include <QSettings>
84 #include <QDrag>
85 #include <QDropEvent>
86 #include <QMimeData>
87 #include <QDesktopServices>
88 #include <QPointer>
89
90
91
92
93 namespace Rosegarden
94 {
95
96 const int AudioManagerDialog::m_maxPreviewWidth = 100;
97 const int AudioManagerDialog::m_previewHeight = 30;
98 const char* const AudioManagerDialog::m_listViewLayoutName = "AudioManagerDialog Layout";
99
AudioManagerDialog(QWidget * parent,RosegardenDocument * doc)100 AudioManagerDialog::AudioManagerDialog(QWidget *parent,
101 RosegardenDocument *doc):
102 QMainWindow(parent),
103 m_doc(doc),
104 m_playingAudioFile(0),
105 m_audioPlayingDialog(nullptr),
106 m_playTimer(new QTimer(this)),
107 m_audiblePreview(true)
108 {
109 setWindowTitle(tr("Audio File Manager"));
110 this->setAttribute(Qt::WA_DeleteOnClose);
111 setWindowIcon(IconLoader::loadPixmap("window-audio-manager"));
112 setMinimumWidth(800);
113
114 QWidget *centralWidget = new QWidget;
115 setCentralWidget(centralWidget);
116
117 QVBoxLayout *boxLayout = new QVBoxLayout;
118 centralWidget->setLayout(boxLayout);
119
120 boxLayout->setContentsMargins(10, 10, 10, 10);
121 boxLayout->setSpacing(5);
122
123 m_sampleRate = RosegardenSequencer::getInstance()->getSampleRate();
124
125 m_fileList = new AudioListView(centralWidget); // internal class needs parent (?)
126 m_fileList->setSelectionMode(QAbstractItemView::SingleSelection);
127 m_fileList->setSelectionBehavior(QAbstractItemView::SelectRows);
128 m_fileList->setIconSize(QSize(m_maxPreviewWidth, m_previewHeight));
129
130 boxLayout->addWidget(m_fileList);
131
132 m_wrongSampleRates = new WarningGroupBox;
133 QVBoxLayout *warningBox = new QVBoxLayout;
134 m_wrongSampleRates->setLayout(warningBox);
135 QLabel *warning = new QLabel(tr("<qt><p><img src=\":pixmaps/tooltip/warning.png\"></img> <b>Audio files marked with an asterisk (*) are encoded at a sample rate different from that of the JACK audio server.</b></p><p>Rosegarden will play them at the correct speed, but they will sound terrible. Please consider resampling these files externally, or adjusting the sample rate of the JACK server.</p></qt>"));
136 warning->setWordWrap(true);
137 warningBox->addWidget(warning);
138
139 boxLayout->addWidget(m_wrongSampleRates);
140 m_wrongSampleRates->hide();
141
142 // file menu
143 createAction("add_audio", SLOT(slotAdd()));
144 createAction("export_audio", SLOT(slotExportAudio()));
145 createAction("file_close", SLOT(slotClose()));
146
147 // edit menu
148 createAction("remove_audio", SLOT(slotRemove()));
149 createAction("remove_all_audio", SLOT(slotRemoveAll()));
150 createAction("remove_all_unused_audio", SLOT(slotRemoveAllUnused()));
151 createAction("delete_unused_audio", SLOT(slotDeleteUnused()));
152
153 // action menu
154 createAction("preview_audio", SLOT(slotPlayPreview()));
155 createAction("insert_audio", SLOT(slotInsert()));
156 createAction("distribute_audio", SLOT(slotDistributeOnMidiSegment()));
157
158 // help menu
159 createAction("audio_help", SLOT(slotHelpRequested()));
160 createAction("help_about_app", SLOT(slotHelpAbout()));
161
162
163 //!!! oh now hang on, does this one work?
164
165 // (No, I've never heard of it until poking around in this code. Julie
166 // hadn't either when she redid the old KXMLGUI stuff for us. I think we've
167 // stumbled across a half eaten bagel here)
168 createAction("distribute_audio", SLOT(slotDistributeOnMidiSegment()));
169
170 // Set the column names
171 //
172 //
173 QStringList sl;
174
175 sl << tr("Name"); // 0
176 sl << tr("Duration"); // 1
177 sl << tr("Envelope"); // 2
178 sl << tr("Sample rate"); // 3
179 sl << tr("Channels"); // 4
180 sl << tr("Resolution"); // 5
181 sl << tr("File"); // 6
182
183 m_fileList->setColumnCount(7);
184 m_fileList->setHeaderItem(new QTreeWidgetItem(sl));
185
186 m_fileList->setSortingEnabled(true);
187
188 //
189 // connect selection mechanism
190 connect(m_fileList, &QTreeWidget::itemSelectionChanged,
191 this, &AudioManagerDialog::slotSelectionChanged);
192
193 connect(m_fileList, SIGNAL(dropped(QDropEvent*, QTreeWidget*, QList<QUrl>)),
194 SLOT(slotDropped(QDropEvent*, QTreeWidget*, QList<QUrl>)));
195
196
197 //
198 // setup local shortcuts
199 //
200
201 // delete
202 //
203 m_shortcuts = new QShortcut(QKeySequence(Qt::Key_Delete), this);
204 connect(m_shortcuts, &QShortcut::activated, this, &AudioManagerDialog::slotRemove);
205
206
207 slotPopulateFileList();
208
209 // Connect command history for updates
210 //
211 connect(CommandHistory::getInstance(), SIGNAL(commandExecuted()),
212 this, SLOT(slotCommandExecuted()));
213
214 connect(m_playTimer, &QTimer::timeout,
215 this, &AudioManagerDialog::slotCancelPlayingAudio);
216
217
218 createMenusAndToolbars("audiomanager.rc"); //@@@ JAS orig. 0
219
220 updateActionState(false);
221
222 QSettings settings;
223 settings.beginGroup(WindowGeometryConfigGroup);
224 this->restoreGeometry(settings.value("Audio_File_Manager").toByteArray());
225 settings.endGroup();
226
227 setAttribute(Qt::WA_DeleteOnClose);
228 }
229
230
slotFileItemActivated()231 void slotFileItemActivated(){
232
233 return;
234 }
235
236
~AudioManagerDialog()237 AudioManagerDialog::~AudioManagerDialog()
238 {
239 //RG_DEBUG << "*** dtor";
240
241 // m_fileList->saveLayout(m_listViewLayoutName); //&&&
242 QSettings settings;
243 settings.beginGroup(WindowGeometryConfigGroup);
244 settings.setValue("Audio_File_Manager", this->saveGeometry());
245 settings.endGroup();
246 }
247
248 void
slotPopulateFileList()249 AudioManagerDialog::slotPopulateFileList()
250 {
251 // create pixmap of given size
252 QPixmap *audioPixmap = new QPixmap(m_maxPreviewWidth, m_previewHeight);
253
254 // Store last selected item if we have one
255 //
256 AudioListItem *selectedItem =
257 dynamic_cast<AudioListItem *>(m_fileList->currentItem());
258
259 AudioFileId lastId = 0;
260 Segment *lastSegment = nullptr;
261 bool findSelection = false;
262 bool foundSelection = false;
263
264 if (selectedItem) {
265 lastId = selectedItem->getId();
266 lastSegment = selectedItem->getSegment();
267 findSelection = true;
268 }
269
270 // We don't want the selection changes to be propagated
271 // to the main view
272 //
273 m_fileList->blockSignals(true);
274
275 // clear file list and disable associated action buttons
276 m_fileList->clear();
277 // AudioListItem* auItem;
278 if (m_doc->getAudioFileManager().begin() ==
279 m_doc->getAudioFileManager().end()) {
280 // Turn off selection and report empty list
281 //
282 // auItem = new AudioListItem(m_fileList, QStringList(tr("<no audio files>")));
283
284 m_fileList->setSelectionMode(QAbstractItemView::NoSelection);
285
286 m_fileList->blockSignals(false);
287 updateActionState(false);
288 return ;
289 }
290
291 // show tree hierarchy
292
293 // enable selection
294 m_fileList->setSelectionMode(QAbstractItemView::SingleSelection);
295
296 // for the sample file length
297 RealTime length;
298
299 // Create a vector of audio Segments only
300 //
301 std::vector<Segment*> segments;
302 std::vector<Segment*>::const_iterator iit;
303
304 for (Composition::iterator it = m_doc->getComposition().begin();
305 it != m_doc->getComposition().end(); ++it) {
306 if ((*it)->getType() == Segment::Audio)
307 segments.push_back(*it);
308 }
309
310 // duration
311 RealTime segmentDuration;
312 bool wrongSampleRates = false;
313
314 for (std::vector<AudioFile*>::const_iterator
315 it = m_doc->getAudioFileManager().begin();
316 it != m_doc->getAudioFileManager().end();
317 ++it) {
318 try {
319 //RG_DEBUG << "slotPopulateFileList(): 1";
320 m_doc->getAudioFileManager().
321 drawPreview((*it)->getId(),
322 RealTime::zeroTime,
323 (*it)->getLength(),
324 audioPixmap);
325 //RG_DEBUG << "slotPopulateFileList(): 2";
326 } catch (const Exception &e) {
327 //RG_DEBUG << "slotPopulateFileList(): 3";
328 audioPixmap->fill(); // white
329 QPainter p(audioPixmap);
330 p.setPen(QColor(Qt::black));
331 p.drawText(10, m_previewHeight / 2, QString("<no preview>"));
332 }
333 //RG_DEBUG << "slotPopulateFileList(): 4";
334
335 //!!! Why isn't the label the label the user assigned to the file?
336 // Why do we allow the user to assign a label at all, then?
337
338 QString label = (*it)->getShortFilename();
339
340 // Set the label, duration, envelope pixmap and filename
341 //
342
343 AudioListItem *item = new AudioListItem(m_fileList, QStringList(label), (*it)->getId());
344 //AudioListItem *item = new AudioListItem(m_fileList, QStringList(label)); //, (*it)->getId());
345
346 // Duration
347 //
348 length = (*it)->getLength();
349 const QString msecs = QString::asprintf("%03d", length.nsec / 1000000);
350 item->setText(1,QString("%1.%2s").arg(length.sec).arg(msecs)); // row, col
351
352 // set start time and duration
353 item->setStartTime(RealTime::zeroTime);
354 item->setDuration(length);
355
356 // Envelope pixmap
357 //
358 item->setIcon(2, QIcon(*audioPixmap)); // row, col
359
360
361 // File location
362 //
363 item->setText(6, m_doc->getAudioFileManager().
364 homeToTilde((*it)->getFilename()));
365
366 // Resolution
367 //
368 item->setText(5, QString("%1 bits").arg((*it)->getBitsPerSample()));
369
370 // Channels
371 //
372 item->setText(4, QString("%1").arg((*it)->getChannels()));
373
374 // Sample rate
375 //
376 if (m_sampleRate != 0 && int((*it)->getSampleRate()) != m_sampleRate) {
377 const QString sRate =
378 QString::asprintf("%.1f KHz *",
379 float((*it)->getSampleRate()) / 1000.0);
380 wrongSampleRates = true;
381 item->setText(3, sRate);
382 } else {
383 const QString sRate =
384 QString::asprintf("%.1f KHz",
385 float((*it)->getSampleRate()) / 1000.0);
386 item->setText(3, sRate);
387 }
388
389 // Test audio file element for selection criteria
390 //
391 if (findSelection && lastSegment == nullptr && lastId == (*it)->getId()) {
392 //m_fileList->setSelected(item, true);
393 m_fileList->setCurrentItem(item);
394
395 findSelection = false;
396 }
397
398 // Add children
399 //
400 for (iit = segments.begin(); iit != segments.end(); ++iit) {
401 if ((*iit)->getAudioFileId() == (*it)->getId()) {
402 AudioListItem *childItem =
403 new AudioListItem(item,
404 QStringList(QString((*iit)->getLabel().c_str())),
405 (*it)->getId());
406 segmentDuration = (*iit)->getAudioEndTime() -
407 (*iit)->getAudioStartTime();
408
409 // store the start time
410 //
411 childItem->setStartTime((*iit)->getAudioStartTime());
412 childItem->setDuration(segmentDuration);
413
414 // Write segment duration
415 //
416 const QString msecs =
417 QString::asprintf("%03d", segmentDuration.nsec / 1000000);
418 childItem->setText(1, QString("%1.%2s")
419 .arg(segmentDuration.sec)
420 .arg(msecs));
421
422 try {
423 m_doc->getAudioFileManager().
424 drawHighlightedPreview((*it)->getId(),
425 RealTime::zeroTime,
426 (*it)->getLength(),
427 (*iit)->getAudioStartTime(),
428 (*iit)->getAudioEndTime(),
429 audioPixmap);
430 } catch (const Exception &e) {
431 // should already be set to "no file"
432 }
433
434 // set pixmap
435 //
436 //childItem->setPixmap(2, *audioPixmap);
437 childItem->setIcon(2, QIcon(*audioPixmap));
438
439 // set segment
440 //
441 childItem->setSegment(*iit);
442
443 if (findSelection && lastSegment == (*iit)) {
444 m_fileList->setCurrentItem(childItem); // select
445 findSelection = false;
446 foundSelection = true;
447 }
448
449 // Add children
450 }
451 }
452 }
453
454 updateActionState(foundSelection);
455
456 if (wrongSampleRates) {
457 m_wrongSampleRates->show();
458 } else {
459 m_wrongSampleRates->hide();
460 }
461
462 m_fileList->blockSignals(false);
463 }
464
465 AudioFile*
getCurrentSelection()466 AudioManagerDialog::getCurrentSelection()
467 {
468 // try and get the selected item
469 QList<QTreeWidgetItem *> til= m_fileList->selectedItems();
470 if (til.isEmpty()){
471 RG_WARNING << "AudioManagerDialog::getCurrentSelection() - til.isEmpty() so we're returning 0 and this game is over. Fail.";
472 return nullptr;
473 }
474 AudioListItem *item = dynamic_cast<AudioListItem*>(til[0]);
475 if (item == nullptr) {
476 RG_WARNING << "AudioManagerDialog::getCurrentSelection() - item == 0 so we're returning 0 and this game is over. Epic fail.";
477 return nullptr;
478 }
479
480 std::vector<AudioFile*>::const_iterator it;
481
482 for (it = m_doc->getAudioFileManager().begin();
483 it != m_doc->getAudioFileManager().end();
484 ++it) {
485 // If we match then return the valid AudioFile
486 //
487 if (item->getId() == (*it)->getId()) {
488 return (*it);
489 } else {
490 RG_WARNING << "AudioManagerDialog::getCurrentSelection() - item->getId() of "
491 << item->getId() << " does not match (*it)->getId() of "
492 << (*it)->getId() << " so you are basically screwed. Sorry about that."
493 ;
494 }
495 }
496
497 RG_WARNING << "AudioManagerDialog::getCurrentSelection() - we tried so hard, but in the end it was all just bricks in the wall.";
498 return nullptr;
499 }
500
501 void
slotExportAudio()502 AudioManagerDialog::slotExportAudio()
503 {
504 WAVAudioFile *sourceFile =
505 dynamic_cast<WAVAudioFile *>(getCurrentSelection());
506
507 if (!sourceFile)
508 return;
509
510 QList<QTreeWidgetItem *> selectedItems = m_fileList->selectedItems();
511
512 if (selectedItems.isEmpty()) {
513 RG_WARNING << "slotExportAudio() - nothing selected!";
514 return;
515 }
516
517 // ??? All we ever look at is the first one.
518 AudioListItem *item = dynamic_cast<AudioListItem *>(selectedItems[0]);
519
520 if (!item)
521 return;
522
523 Segment *segment = item->getSegment();
524
525 QString destFileName =
526 FileDialog::getSaveFileName(
527 this, // parent
528 tr("Save File As"), // caption
529 QDir::currentPath(), // dir
530 sourceFile->getFilename(), // defaultName
531 tr("*.wav|WAV files (*.wav)")); // filter
532
533 if (destFileName.isEmpty())
534 return;
535
536 // Check for a dot extension and append ".wav" if not found
537 // ??? Should use QFileInfo::suffix() to check for an extension.
538 if (destFileName.contains(".") == 0)
539 destFileName += ".wav";
540
541 // Progress Dialog
542 QProgressDialog progressDialog(
543 tr("Exporting audio file..."), // labelText
544 tr("Cancel"), // cancelButtonText
545 0, 0, // min, max
546 this); // parent
547 progressDialog.setWindowTitle(tr("Rosegarden"));
548 progressDialog.setWindowModality(Qt::WindowModal);
549 // We will close anyway when this object goes out of scope.
550 progressDialog.setAutoClose(false);
551 // No cancel button since appendSamples() doesn't support progress.
552 progressDialog.setCancelButton(nullptr);
553 // Just force the progress dialog up.
554 // Both Qt4 and Qt5 have bugs related to delayed showing of progress
555 // dialogs. In Qt4, the dialog sometimes won't show. In Qt5, KDE
556 // based distros might lock up. See Bug #1546.
557 progressDialog.show();
558
559 RealTime clipStartTime = RealTime::zeroTime;
560 RealTime clipDuration = sourceFile->getLength();
561
562 if (segment) {
563 clipStartTime = segment->getAudioStartTime();
564 clipDuration = segment->getAudioEndTime() - clipStartTime;
565 }
566
567 WAVAudioFile destFile(
568 destFileName,
569 sourceFile->getChannels(),
570 sourceFile->getSampleRate(),
571 sourceFile->getBytesPerSecond(),
572 sourceFile->getBytesPerFrame(),
573 sourceFile->getBitsPerSample());
574
575 if (sourceFile->open() == false)
576 return;
577
578 destFile.write();
579
580 sourceFile->scanTo(clipStartTime);
581
582 // appendSamples() takes the longest. Would be nice if it would
583 // take a progress dialog.
584 destFile.appendSamples(sourceFile->getSampleFrameSlice(clipDuration));
585
586 destFile.close();
587 sourceFile->close();
588 }
589
590 void
slotRemove()591 AudioManagerDialog::slotRemove()
592 {
593 QList<QTreeWidgetItem *> selectedTreeItems = m_fileList->selectedItems();
594
595 // If nothing is selected, bail.
596 // ??? Why not disable the action when there is no selection?
597 // Then this will never happen.
598 if (selectedTreeItems.isEmpty())
599 return;
600
601 AudioListItem *item = dynamic_cast<AudioListItem *>(selectedTreeItems[0]);
602 if (!item)
603 return;
604
605 // If we're on a Segment then delete it from the Composition
606 // and refresh the list.
607 if (item->getSegment()) {
608 // Get the next item to highlight
609 QTreeWidgetItem *newTreeItem = m_fileList->itemBelow(item);
610
611 // Nothing below? Try above.
612 if (!newTreeItem)
613 newTreeItem = m_fileList->itemAbove(item);
614
615 const AudioListItem *newAudioItem =
616 dynamic_cast<const AudioListItem *>(newTreeItem);
617
618 // If the item was an AudioListItem, and it is a Segment item...
619 if (newAudioItem && newAudioItem->getSegment()) {
620 // Jump to new selection
621 setSelected(newAudioItem->getId(),
622 newAudioItem->getSegment(),
623 true); // propagate
624 }
625
626 // Delete the Segment from the Composition.
627 SegmentSelection selection;
628 selection.insert(item->getSegment());
629 emit deleteSegments(selection);
630
631 return;
632 }
633
634 // An audio file item is selected in the tree...
635
636 const AudioFile *audioFile = getCurrentSelection();
637 if (!audioFile)
638 return;
639
640 // remove segments along with audio file
641 //
642 AudioFileId id = audioFile->getId();
643 SegmentSelection selection;
644 Composition &comp = m_doc->getComposition();
645
646 bool haveSegments = false;
647 for (Composition::iterator it = comp.begin(); it != comp.end(); ++it) {
648 if ((*it)->getType() == Segment::Audio &&
649 (*it)->getAudioFileId() == id) {
650 haveSegments = true;
651 break;
652 }
653 }
654
655 if (haveSegments) {
656
657 QString question = tr("This will unload audio file \"%1\" and remove all associated segments. Are you sure?")
658 .arg(audioFile->getFilename());
659
660 // Ask the question
661 int reply = QMessageBox::warning(this, tr("Rosegarden"), question, QMessageBox::Yes | QMessageBox::Cancel , QMessageBox::Cancel);
662
663 if (reply != QMessageBox::Yes)
664 return ;
665 }
666
667 for (Composition::iterator it = comp.begin(); it != comp.end(); ++it) {
668 if ((*it)->getType() == Segment::Audio &&
669 (*it)->getAudioFileId() == id)
670 selection.insert(*it);
671 }
672 emit deleteSegments(selection);
673
674 m_doc->notifyAudioFileRemoval(id);
675
676 m_doc->getAudioFileManager().removeFile(id);
677
678 // tell the sequencer
679 emit deleteAudioFile(id);
680
681 // repopulate
682 slotPopulateFileList();
683 }
684
685 void
slotPlayPreview()686 AudioManagerDialog::slotPlayPreview()
687 {
688 AudioFile *audioFile = getCurrentSelection();
689
690 QList<QTreeWidgetItem*> til = m_fileList->selectedItems();
691 if (til.isEmpty()) {
692 RG_WARNING << "AudioManagerDialog::slotPlayPreview() - nothing selected!";
693 return;
694 }
695 AudioListItem *item = dynamic_cast<AudioListItem*>(til[0]);
696
697 if (item == nullptr || audioFile == nullptr)
698 return ;
699
700 // store the audio file we're playing
701 m_playingAudioFile = audioFile->getId();
702
703 // tell the sequencer
704 emit playAudioFile(audioFile->getId(),
705 item->getStartTime(),
706 item->getDuration());
707
708 // now open up the playing dialog
709 //
710 m_audioPlayingDialog =
711 new AudioPlayingDialog(this, audioFile->getFilename());
712
713 // Setup timer to pop down dialog after file has completed
714 //
715 int msecs = item->getDuration().sec * 1000 +
716 item->getDuration().nsec / 1000000;
717 m_playTimer->setSingleShot(true);
718 m_playTimer->start(msecs);
719
720 // just execute
721 //
722 if (m_audioPlayingDialog->exec() == QDialog::Rejected)
723 emit cancelPlayingAudioFile(m_playingAudioFile);
724
725 delete m_audioPlayingDialog;
726 m_audioPlayingDialog = nullptr;
727
728 m_playTimer->stop();
729
730 }
731
732 void
slotCancelPlayingAudio()733 AudioManagerDialog::slotCancelPlayingAudio()
734 {
735 //std::cout << "AudioManagerDialog::slotCancelPlayingAudio";
736 if (m_audioPlayingDialog) {
737 m_playTimer->stop();
738 delete m_audioPlayingDialog;
739 m_audioPlayingDialog = nullptr;
740 }
741 }
742
743 void
slotAdd()744 AudioManagerDialog::slotAdd()
745 {
746 QString extensionList = tr("WAV files") + " (*.wav *.WAV);;" +
747 tr("All files") + " (*)";
748
749 if (RosegardenMainWindow::self()->haveAudioImporter()) {
750 //!!! This list really needs to come from the importer helper program
751 // (which has an option to supply it -- we just haven't recorded it)
752 //
753 extensionList = tr("Audio files") + " (*.wav *.flac *.ogg *.mp3 *.WAV *.FLAC *.OGG *.MP3)" + ";;" +
754 tr("WAV files") + " (*.wav *.WAV)" + ";;" +
755 tr("FLAC files") + " (*.flac *.FLAC)" + ";;" +
756 tr("Ogg files") + " (*.ogg *.OGG)" + ";;" +
757 tr("MP3 files") + " (*.mp3 *.MP3)" + ";;" +
758 tr("All files") + " (*)";
759 }
760
761 // default to ~ for files if nothing previously stored in settings
762 QSettings settings;
763 settings.beginGroup(LastUsedPathsConfigGroup);
764 QString directory = settings.value("add_audio_file", QDir::homePath()).toString();
765
766 //RG_DEBUG << "slotAdd(): using stored/default path: " << qstrtostr(directory);
767
768 const QStringList fileList = FileDialog::getOpenFileNames(this, tr("Select one or more audio files"), directory, extensionList);
769
770 QDir d;
771 for (int i = 0 ; i < fileList.size(); i++) {
772 addFile(QUrl::fromLocalFile(fileList.at(i)));
773 d = QFileInfo(fileList.at(i)).dir();
774 }
775
776 // pick the directory from the last URL encountered to save for future
777 // reference, but don't store anything if no URLs were encountered (ie. the
778 // user hit cancel on the file dialog without choosing anything)
779 directory = d.canonicalPath();
780
781 if (!fileList.isEmpty()) {
782 settings.setValue("add_audio_file", directory);
783
784 //RG_DEBUG << "slotAdd(): storing path: " << qstrtostr(directory);
785 } else {
786 //RG_DEBUG << "slotAdd(): URL list was empty. No files added. Not storing path.";
787 }
788
789 settings.endGroup();
790 }
791
792 void
updateActionState(bool haveSelection)793 AudioManagerDialog::updateActionState(bool haveSelection)
794 {
795 //RG_DEBUG << "updateActionState(" << (haveSelection ? "true" : "false") << ")";
796
797 if (m_doc->getAudioFileManager().begin() ==
798 m_doc->getAudioFileManager().end()) {
799 leaveActionState("have_audio_files"); //@@@ JAS orig. KXMLGUIClient::StateReverse
800 } else {
801 enterActionState("have_audio_files"); //@@@ JAS orig. KXMLGUIClient::StateNoReverse
802 }
803
804 if (haveSelection) {
805
806 enterActionState("have_audio_selected"); //@@@ JAS orig. KXMLGUIClient::StateNoReverse
807
808 if (m_audiblePreview) {
809 enterActionState("have_audible_preview"); //@@@ JAS orig. KXMLGUIClient::StateNoReverse
810 } else {
811 leaveActionState("have_audible_preview"); //@@@ JAS orig. KXMLGUIClient::StateReverse
812 }
813
814 if (isSelectedTrackAudio()) {
815 enterActionState("have_audio_insertable"); //@@@ JAS orig. KXMLGUIClient::StateNoReverse
816 } else {
817 leaveActionState("have_audio_insertable"); //@@@ JAS orig. KXMLGUIClient::StateReverse
818 }
819
820 } else {
821 leaveActionState("have_audio_selected"); //@@@ JAS orig. KXMLGUIClient::StateReverse
822 leaveActionState("have_audio_insertable"); //@@@ JAS orig. KXMLGUIClient::StateReverse
823 leaveActionState("have_audible_preview"); //@@@ JAS orig. KXMLGUIClient::StateReverse
824 }
825 }
826
827 void
slotInsert()828 AudioManagerDialog::slotInsert()
829 {
830 //RG_DEBUG << "slotInsert(): begin...";
831
832 AudioFile *audioFile = getCurrentSelection();
833 if (audioFile == nullptr)
834 return ;
835
836 //RG_DEBUG << "slotInsert(): emitting insertAudioSegment()";
837
838 emit insertAudioSegment(audioFile->getId(),
839 RealTime::zeroTime,
840 audioFile->getLength());
841 }
842
843 void
slotRemoveAll()844 AudioManagerDialog::slotRemoveAll()
845 {
846 QString question =
847 tr("This will unload all audio files and remove their associated segments.\nThis action cannot be undone, and associations with these files will be lost.\nFiles will not be removed from your disk.\nAre you sure?");
848
849 int reply = QMessageBox::warning(this, tr("Rosegarden"), question, QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel);
850
851 if (reply != QMessageBox::Yes)
852 return ;
853
854 SegmentSelection selection;
855 Composition &comp = m_doc->getComposition();
856
857 for (Composition::iterator it = comp.begin(); it != comp.end(); ++it) {
858 if ((*it)->getType() == Segment::Audio)
859 selection.insert(*it);
860 }
861 // delete segments
862 emit deleteSegments(selection);
863
864 for (std::vector<AudioFile*>::const_iterator
865 aIt = m_doc->getAudioFileManager().begin();
866 aIt != m_doc->getAudioFileManager().end(); ++aIt) {
867 m_doc->notifyAudioFileRemoval((*aIt)->getId());
868 }
869
870 m_doc->getAudioFileManager().clear();
871
872 // and now the audio files
873 emit deleteAllAudioFiles();
874
875 // clear the file list
876 m_fileList->clear();
877 slotPopulateFileList();
878 }
879
880 void
slotRemoveAllUnused()881 AudioManagerDialog::slotRemoveAllUnused()
882 {
883 QString question =
884 tr("This will unload all audio files that are not associated with any segments in this composition.\nThis action cannot be undone, and associations with these files will be lost.\nFiles will not be removed from your disk.\nAre you sure?");
885
886 int reply = QMessageBox::warning(this, tr("Rosegarden"), question,QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel);
887
888 if (reply != QMessageBox::Yes)
889 return ;
890
891 std::set
892 <AudioFileId> audioFiles;
893 Composition &comp = m_doc->getComposition();
894
895 for (Composition::iterator it = comp.begin(); it != comp.end(); ++it) {
896 if ((*it)->getType() == Segment::Audio)
897 audioFiles.insert((*it)->getAudioFileId());
898 }
899
900 std::vector<AudioFileId> toDelete;
901 for (std::vector<AudioFile*>::const_iterator
902 aIt = m_doc->getAudioFileManager().begin();
903 aIt != m_doc->getAudioFileManager().end(); ++aIt) {
904 if (audioFiles.find((*aIt)->getId()) == audioFiles.end())
905 toDelete.push_back((*aIt)->getId());
906 }
907
908 // Delete the audio files from the AFM
909 //
910 for (std::vector<AudioFileId>::iterator dIt = toDelete.begin();
911 dIt != toDelete.end(); ++dIt) {
912
913 m_doc->notifyAudioFileRemoval(*dIt);
914 m_doc->getAudioFileManager().removeFile(*dIt);
915 emit deleteAudioFile(*dIt);
916 }
917
918 // clear the file list
919 m_fileList->clear();
920 slotPopulateFileList();
921 }
922
923 void
slotDeleteUnused()924 AudioManagerDialog::slotDeleteUnused()
925 {
926 std::set
927 <AudioFileId> audioFiles;
928 Composition &comp = m_doc->getComposition();
929
930 for (Composition::iterator it = comp.begin(); it != comp.end(); ++it) {
931 if ((*it)->getType() == Segment::Audio)
932 audioFiles.insert((*it)->getAudioFileId());
933 }
934
935 std::vector<QString> toDelete;
936 std::map<QString, AudioFileId> nameMap;
937
938 for (std::vector<AudioFile*>::const_iterator
939 aIt = m_doc->getAudioFileManager().begin();
940 aIt != m_doc->getAudioFileManager().end(); ++aIt) {
941 if (audioFiles.find((*aIt)->getId()) == audioFiles.end()) {
942 toDelete.push_back((*aIt)->getFilename());
943 nameMap[(*aIt)->getFilename()] = (*aIt)->getId();
944 }
945 }
946
947 UnusedAudioSelectionDialog *dialog = new UnusedAudioSelectionDialog
948 (this,
949 tr("The following audio files are not used in the current composition.\n\nPlease select the ones you wish to delete permanently from the hard disk.\n"),
950 toDelete);
951
952 if (dialog->exec() == QDialog::Accepted) {
953
954 std::vector<QString> names = dialog->getSelectedAudioFileNames();
955
956 if (names.size() > 0) {
957
958 QString question =
959 tr("<qt>About to delete %n audio file(s) permanently from the hard disk.<br>This action cannot be undone, and there will be no way to recover the files.<br>Are you sure?</qt>", "", names.size());
960
961 int reply = QMessageBox::warning(this, tr("Rosegarden"), question, QMessageBox::Yes | QMessageBox::Cancel, QMessageBox::Cancel);
962
963 if (reply != QMessageBox::Yes) {
964 delete dialog;
965 return ;
966 }
967
968 for (unsigned int i = 0; i < names.size(); ++i) {
969 //RG_DEBUG << i << ": " << names[i];
970 QFile file(names[i]);
971 if (!file.remove()) {
972 QMessageBox::critical(this, tr("Rosegarden"), tr("File %1 could not be deleted.").arg(names[i]));
973 } else {
974 if (nameMap.find(names[i]) != nameMap.end()) {
975 m_doc->getAudioFileManager().removeFile(nameMap[names[i]]);
976 emit deleteAudioFile(nameMap[names[i]]);
977 } else {
978 RG_WARNING << "slotDeleteUnused(): WARNING: Audio file name " << names[i] << " not in name map";
979 }
980
981 QFile peakFile(QString("%1.pk").arg(names[i]));
982 peakFile.remove();
983 }
984 }
985 }
986 }
987
988 m_fileList->clear();
989 slotPopulateFileList();
990
991 delete dialog;
992 }
993
994 void
slotRename()995 AudioManagerDialog::slotRename()
996 {
997 AudioFile *audioFile = getCurrentSelection();
998
999 if (audioFile == nullptr)
1000 return ;
1001
1002 bool ok = false;
1003
1004 QString newText = InputDialog::getText(this,
1005 tr("Change Audio File label"),
1006 tr("Enter new label"),
1007 LineEdit::Normal,
1008 QString(audioFile->getLabel().c_str()),
1009 &ok);
1010
1011 if (ok && !newText.isEmpty())
1012 audioFile->setLabel(qstrtostr(newText));
1013
1014 slotPopulateFileList();
1015 }
1016
1017
1018 /*
1019 void AudioManagerDialog::slotItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous)
1020 {
1021 //RG_DEBUG << "Warning: AudioManagerDialog::slotItemChanged not implemented";
1022 return;
1023 }
1024 */
1025
1026 //void AudioManagerDialog::slotSelectionChanged(QTreeWidgetItem *item)
slotSelectionChanged()1027 void AudioManagerDialog::slotSelectionChanged()
1028 {
1029 AudioListItem *aItem = nullptr;
1030 //AudioListItem *aItem = dynamic_cast<AudioListItem*>(item);
1031
1032 QList<QTreeWidgetItem *> itemsx = m_fileList->selectedItems();
1033 if (itemsx.count() > 0){
1034 aItem = dynamic_cast<AudioListItem*>(itemsx.at(0));
1035 }
1036
1037 // en/disable Actions
1038 //QAction *ea = findAction("export_audio");
1039 //if (ea) ea->setEnabled("false");
1040
1041 // If we're on a segment then send a "select" signal
1042 // and enable appropriate buttons.
1043 //
1044 if (aItem && aItem->getSegment()) {
1045
1046 //### required to enable it?
1047 //if (ea) ea->setEnabled("true");
1048
1049 SegmentSelection selection;
1050 selection.insert(aItem->getSegment());
1051 emit segmentsSelected(selection);
1052 }
1053
1054 updateActionState(aItem != nullptr);
1055 }
1056
1057 void
setSelected(AudioFileId id,const Segment * segment,bool propagate)1058 AudioManagerDialog::setSelected(AudioFileId id,
1059 const Segment *segment,
1060 bool propagate)
1061 {
1062 // note: this iterates over topLevelItems and childItems too.
1063 // I hope that's what we want to do (?)
1064 // otherwise re-code to iterate over topLevelItems only.
1065 QTreeWidgetItemIterator treeItemIter(m_fileList,
1066 QTreeWidgetItemIterator::All);
1067
1068 QTreeWidgetItem *treeItem = *treeItemIter;
1069
1070 while (treeItem) {
1071
1072 AudioListItem *audioItem = dynamic_cast<AudioListItem *>(treeItem);
1073
1074 if (audioItem) {
1075 if ((audioItem->getId() == id) &&
1076 (audioItem->getSegment() == segment)) {
1077
1078 selectFileListItemNoSignal(treeItem);
1079
1080 // Only propagate to compositionview if asked to
1081 if (propagate) {
1082 SegmentSelection selection;
1083 selection.insert(audioItem->getSegment());
1084 emit segmentsSelected(selection);
1085 }
1086
1087 return;
1088
1089 }
1090
1091 }
1092
1093 ++treeItemIter;
1094 treeItem = *treeItemIter;
1095
1096 }
1097
1098 }
1099
1100
1101 void
selectFileListItemNoSignal(QTreeWidgetItem * it)1102 AudioManagerDialog::selectFileListItemNoSignal(QTreeWidgetItem* it)
1103 {
1104 m_fileList->blockSignals(true);
1105
1106 if (it) {
1107 // m_fileList->ensureItemVisible(it);
1108 m_fileList->scrollToItem(it, QAbstractItemView::PositionAtTop);
1109 // m_fileList->setSelected(it, true);
1110 m_fileList->setCurrentItem(it);
1111 updateActionState(true);
1112 } else {
1113 m_fileList->clearSelection();
1114 }
1115
1116 m_fileList->blockSignals(false);
1117 }
1118
1119 void
slotCommandExecuted()1120 AudioManagerDialog::slotCommandExecuted()
1121 {
1122 slotPopulateFileList();
1123 }
1124
1125 void
slotSegmentSelection(const SegmentSelection & segments)1126 AudioManagerDialog::slotSegmentSelection(
1127 const SegmentSelection &segments)
1128 {
1129 const Segment *segment = nullptr;
1130
1131 for (SegmentSelection::const_iterator it = segments.begin();
1132 it != segments.end(); ++it) {
1133 if ((*it)->getType() == Segment::Audio) {
1134 // Only get one audio segment
1135 if (segment == nullptr)
1136 segment = *it;
1137 else
1138 segment = nullptr;
1139 }
1140
1141 }
1142
1143 if (segment) {
1144 // We don't propagate this segment setting to the canvas
1145 // as we probably got called from there.
1146 //
1147 setSelected(segment->getAudioFileId(), segment, false);
1148 } else {
1149 selectFileListItemNoSignal(nullptr);
1150 }
1151
1152 }
1153
1154 void
slotCancelPlayingAudioFile()1155 AudioManagerDialog::slotCancelPlayingAudioFile()
1156 {
1157 emit cancelPlayingAudioFile(m_playingAudioFile);
1158 }
1159
1160 void
closePlayingDialog(AudioFileId id)1161 AudioManagerDialog::closePlayingDialog(AudioFileId id)
1162 {
1163 //std::cout << "AudioManagerDialog::closePlayingDialog";
1164 if (m_audioPlayingDialog && id == m_playingAudioFile) {
1165 m_playTimer->stop();
1166 delete m_audioPlayingDialog;
1167 m_audioPlayingDialog = nullptr;
1168 }
1169
1170 }
1171
1172 bool
addFile(const QUrl & kurl)1173 AudioManagerDialog::addFile(const QUrl& kurl)
1174 {
1175 AudioFileId id = 0;
1176
1177 AudioFileManager &aFM = m_doc->getAudioFileManager();
1178
1179 if (!RosegardenMainWindow::self()->testAudioPath(tr("importing an audio file that needs to be converted or resampled"))) {
1180 return false;
1181 }
1182
1183 // If multiple audio files are added concurrently, this implementation
1184 // looks funny to the user, but it is functional for now. NO time for
1185 // a more robust solution.
1186 // ??? By "looks funny" I assume this means that it pops up a new
1187 // progress dialog for each file being added. Moving the
1188 // QProgressDialog up the call stack shouldn't be too hard.
1189 // That might cover one of the cases anyway.
1190
1191 // Progress Dialog
1192 // Note: The label text and range will be set later as needed.
1193 QProgressDialog progressDialog(
1194 tr("Adding audio file..."), // labelText
1195 tr("Cancel"), // cancelButtonText
1196 0, 100, // min, max
1197 this); // parent
1198 progressDialog.setWindowTitle(tr("Rosegarden"));
1199 progressDialog.setWindowModality(Qt::WindowModal);
1200 // Don't want to auto close since this is a multi-step
1201 // process. Any of the steps may set progress to 100. We
1202 // will close anyway when this object goes out of scope.
1203 progressDialog.setAutoClose(false);
1204 // Just force the progress dialog up.
1205 // Both Qt4 and Qt5 have bugs related to delayed showing of progress
1206 // dialogs. In Qt4, the dialog sometimes won't show. In Qt5, KDE
1207 // based distros might lock up. See Bug #1546.
1208 progressDialog.show();
1209
1210 aFM.setProgressDialog(&progressDialog);
1211
1212 // Flush the event queue.
1213 qApp->processEvents(QEventLoop::AllEvents);
1214
1215 try {
1216 id = aFM.importURL(kurl, m_sampleRate);
1217 } catch (const AudioFileManager::BadAudioPathException &e) {
1218 QString errorString = tr("Failed to add audio file. ") + strtoqstr(e.getMessage());
1219 QMessageBox::warning(this, tr("Rosegarden"), errorString);
1220 return false;
1221 } catch (const SoundFile::BadSoundFileException &e) {
1222 QString errorString = tr("Failed to add audio file. ") + strtoqstr(e.getMessage());
1223 QMessageBox::warning(this, tr("Rosegarden"), errorString);
1224 return false;
1225 }
1226
1227 try {
1228 aFM.generatePreview(id);
1229 } catch (const Exception &e) {
1230 QString message = strtoqstr(e.getMessage()) + "\n\n" +
1231 tr("Try copying this file to a directory where you have write permission and re-add it");
1232 QMessageBox::information(this, tr("Rosegarden"), message);
1233 }
1234
1235 slotPopulateFileList();
1236
1237 // tell the sequencer
1238 emit addAudioFile(id);
1239
1240 return true;
1241 }
1242
1243
1244 void
slotDropped(QDropEvent *,QTreeWidget *,const QList<QUrl> & sl)1245 AudioManagerDialog::slotDropped(QDropEvent* /* event */, QTreeWidget*, const QList<QUrl> &sl){
1246 /// signaled from AudioListView on dropEvent, sl = list of items (URLs)
1247 if( sl.empty() ) return;
1248
1249 // iterate over dropped URIs
1250 for( int i=0; i<sl.count(); i++ ) {
1251 //RG_DEBUG << "slotDropped() - Adding DroppedFile " << sl.at(i);
1252 addFile( sl.at(i) );
1253 }
1254 }
1255
1256 //void
1257 //AudioManagerDialog::slotDropped(QDropEvent *event, QTreeWidgetItem*)
1258 //{
1259
1260 /*
1261 //QStrList uri;
1262 QList<QString> uri;
1263
1264 // see if we can decode a URI.. if not, just ignore it
1265 // if (QUriDrag::decode(event, uri)) { //&&& QUriDrag, implement drag/drop
1266
1267 // okay, we have a URI.. process it
1268 // for (QString url = uri.first(); !url.isEmpty(); url = uri.next()) { //!!! this one is really weird and uncertain
1269 for (int i=0; i < uri.size(); i++){
1270 QString url = uri.at(i);
1271
1272 RG_DEBUG << "AudioManagerDialog::dropEvent() : got " << url;
1273
1274 addFile(QUrl(url));
1275 }
1276 // }// end if QUriDrag
1277 */
1278 //}
1279
1280 void
closeEvent(QCloseEvent * e)1281 AudioManagerDialog::closeEvent(QCloseEvent *e)
1282 {
1283 //RG_DEBUG << "closeEvent()\n";
1284 emit closing();
1285 QMainWindow::closeEvent(e);
1286 }
1287
1288 void
slotClose()1289 AudioManagerDialog::slotClose()
1290 {
1291 //RG_DEBUG << "slotClose()";
1292 emit closing();
1293 close();
1294 }
1295
1296 void
setAudioSubsystemStatus(bool ok)1297 AudioManagerDialog::setAudioSubsystemStatus(bool ok)
1298 {
1299 // We can do something more fancy in the future but for the moment
1300 // this will suffice.
1301 //
1302 m_audiblePreview = ok;
1303 }
1304
1305 bool
addAudioFile(const QString & filePath)1306 AudioManagerDialog::addAudioFile(const QString &filePath)
1307 {
1308 QString fp = QFileInfo(filePath).absoluteFilePath();
1309 //RG_DEBUG << "addAudioFile(): fp =" << fp;
1310 return addFile(QUrl::fromLocalFile(fp));
1311 }
1312
1313 bool
isSelectedTrackAudio()1314 AudioManagerDialog::isSelectedTrackAudio()
1315 {
1316 Composition &comp = m_doc->getComposition();
1317 Studio &studio = m_doc->getStudio();
1318
1319 TrackId currentTrackId = comp.getSelectedTrack();
1320 Track *track = comp.getTrackById(currentTrackId);
1321
1322 if (track) {
1323 InstrumentId ii = track->getInstrument();
1324 Instrument *instrument = studio.getInstrumentById(ii);
1325
1326 if (instrument &&
1327 instrument->getType() == Instrument::Audio)
1328 return true;
1329 }
1330
1331 return false;
1332
1333 }
1334
1335 void
slotDistributeOnMidiSegment()1336 AudioManagerDialog::slotDistributeOnMidiSegment()
1337 {
1338 //RG_DEBUG << "slotDistributeOnMidiSegment()";
1339
1340 //Composition &comp = m_doc->getComposition();
1341
1342 QList<RosegardenMainViewWidget*> viewList_ = m_doc->getViewList();
1343 QListIterator<RosegardenMainViewWidget*> viewList(viewList_);
1344
1345 RosegardenMainViewWidget *w = nullptr;
1346 SegmentSelection selection;
1347
1348 viewList.toFront();
1349 while (viewList.hasNext()){
1350 w = viewList.next();
1351 selection = w->getSelection();
1352 }
1353
1354 // Store the insert times in a local vector
1355 //
1356 std::vector<timeT> insertTimes;
1357
1358 for (SegmentSelection::iterator i = selection.begin();
1359 i != selection.end(); ++i) {
1360 // For MIDI (Internal) Segments only of course
1361 //
1362 if ((*i)->getType() == Segment::Internal) {
1363 for (Segment::iterator it = (*i)->begin(); it != (*i)->end(); ++it) {
1364 if ((*it)->isa(Note::EventType))
1365 insertTimes.push_back((*it)->getAbsoluteTime());
1366 }
1367 }
1368 }
1369
1370 #if 0
1371 for (unsigned int i = 0; i < insertTimes.size(); ++i) {
1372 RG_DEBUG << "slotDistributeOnMidiSegment(): Insert audio segment at " << insertTimes[i];
1373 }
1374 #endif
1375 }
1376
1377 void
slotHelpRequested()1378 AudioManagerDialog::slotHelpRequested()
1379 {
1380 // TRANSLATORS: if the manual is translated into your language, you can
1381 // change the two-letter language code in this URL to point to your language
1382 // version, eg. "http://rosegardenmusic.com/wiki/doc:audioManager-es" for the
1383 // Spanish version. If your language doesn't yet have a translation, feel
1384 // free to create one.
1385 QString helpURL = tr("http://rosegardenmusic.com/wiki/doc:audioManager-en");
1386 QDesktopServices::openUrl(QUrl(helpURL));
1387 }
1388
1389
1390 void
slotHelpAbout()1391 AudioManagerDialog::slotHelpAbout()
1392 {
1393 new AboutDialog(this);
1394 }
1395 }
1396