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 #include "LyricEditDialog.h"
20
21
22 #include "base/Event.h"
23 #include "base/BaseProperties.h"
24 #include "misc/Strings.h"
25 #include "misc/Debug.h"
26 #include "misc/ConfigGroups.h"
27 #include "base/Composition.h"
28 #include "base/NotationTypes.h"
29 #include "base/Segment.h"
30
31 #include <QComboBox>
32 #include <QDesktopServices>
33 #include <QDialog>
34 #include <QDialogButtonBox>
35 #include <QGroupBox>
36 #include <QLabel>
37 #include <QMenu>
38 #include <QMessageBox>
39 #include <QPushButton>
40 #include <QRegularExpression>
41 #include <QString>
42 #include <QTextEdit>
43 #include <QUrl>
44 #include <QVBoxLayout>
45 #include <QWidget>
46
47
48 namespace Rosegarden
49 {
50
LyricEditDialog(QWidget * parent,std::vector<Segment * > & segments,Segment * segment)51 LyricEditDialog::LyricEditDialog(QWidget *parent,
52 std::vector<Segment *> &segments,
53 Segment *segment) :
54 QDialog(parent),
55 m_segment(segment),
56 m_segmentSelectMenu(nullptr),
57 m_descr1(nullptr),
58 m_descr2(nullptr),
59 m_verseCount(0),
60 m_previousVerseCount(0)
61 {
62 setModal(true);
63 setWindowTitle(tr("Edit Lyrics"));
64
65 // If several segments, setup a menu to change the selected one
66 if (segments.size() > 1) {
67 m_segmentSelectMenu = new QMenu(this);
68 m_menuActionsMap.clear();
69 Composition *comp = m_segment->getComposition();
70
71 // Associate a description with each segment and use a multimap
72 // to sort them
73 std::multimap<QString, Segment *> segDescriptionMap;
74
75 for (std::vector<Segment *>::iterator it = segments.begin();
76 it != segments.end(); ++it) {
77
78 // Get segment characteristics
79 timeT segStart = (*it)->getStartTime();
80 timeT segEnd = (*it)->getEndMarkerTime();
81 int barStart = comp->getBarNumber(segStart) + 1;
82 int barEnd = comp->getBarNumber(segEnd - 1) + 1;
83 QString label = strtoqstr((*it)->getLabel());
84
85 // Shorten too long labels
86 if (label.length() > 53) label = label.left(50) + "...";
87
88 // Create the description
89 QString segDescr = QString(tr("Track %1, bar %2 to %3: \"%4\""))
90 .arg(comp->getTrackPositionById((*it)->getTrack()) + 1)
91 .arg(barStart)
92 .arg(barEnd)
93 .arg(label);
94
95 // Insert description and segment in the map
96 segDescriptionMap.insert(std::pair<QString, Segment *>(segDescr, *it));
97 }
98
99 // Populate the menu with an entry for each segment.
100 // Reading the segments from the multimap sorts them by description.
101 for (std::multimap<QString, Segment *>::iterator it = segDescriptionMap.begin();
102 it != segDescriptionMap.end(); ++it) {
103 m_menuActionsMap[m_segmentSelectMenu->addAction((*it).first)] = (*it).second;
104 }
105 }
106
107 // Begin dialog layout
108 QGridLayout *metagrid = new QGridLayout;
109 setLayout(metagrid);
110 QWidget *vbox = new QWidget(this);
111 QVBoxLayout *vboxLayout = new QVBoxLayout;
112 metagrid->addWidget(vbox, 0, 0);
113
114 // Add the following elements of layout only if more than
115 // one segment is opened in the notation editor
116 if (m_segmentSelectMenu) {
117
118 // QLabels to display a description of the selected segment
119 m_descr1 = new QLabel("");
120 vboxLayout->addWidget(m_descr1);
121 m_descr2 = new QLabel("");
122 vboxLayout->addWidget(m_descr2);
123
124 // Write out the description
125 showDescriptionOfSelectedSegment();
126
127 // Add a button to give the user a chance to select another segment
128 QPushButton *selectSegment = new QPushButton(tr("Select another segment"));
129
130 // Avoid button to become exaggeratedly large when dialog is resized
131 QWidget *hb = new QWidget(vbox);
132 QHBoxLayout *hbl = new QHBoxLayout;
133 hb->setLayout(hbl);
134 vboxLayout->addWidget(hb);
135 QFrame *fr = new QFrame(hb);
136 hbl->addWidget(fr);
137 hbl->setStretchFactor(fr, 10);
138 hbl->addWidget(selectSegment);
139
140 // Connect the button to the menu and the menu to the appropriate slot
141 selectSegment->setMenu(m_segmentSelectMenu);
142 connect(m_segmentSelectMenu, &QMenu::triggered,
143 this, &LyricEditDialog::slotSegmentChanged);
144 }
145
146 // Continue dialog layout
147 QGroupBox *groupBox = new QGroupBox( tr("Lyrics for this segment"), vbox );
148 QVBoxLayout *groupBoxLayout = new QVBoxLayout;
149 vboxLayout->addWidget(groupBox);
150 vbox->setLayout(vboxLayout);
151
152 QWidget *hbox = new QWidget(groupBox);
153 QHBoxLayout *hboxLayout = new QHBoxLayout;
154 groupBoxLayout->addWidget(hbox);
155 hboxLayout->setSpacing(5);
156 // new QLabel(tr("Verse:"), hbox);
157 m_verseNumber = new QComboBox( hbox );
158 hboxLayout->addWidget(m_verseNumber);
159 m_verseNumber->setEditable(false);
160 connect(m_verseNumber, SIGNAL(activated(int)), this, SLOT(slotVerseNumberChanged(int)));
161 m_verseAddButton = new QPushButton(tr("Add Verse"), hbox );
162 hboxLayout->addWidget(m_verseAddButton);
163 connect(m_verseAddButton, &QAbstractButton::clicked, this, &LyricEditDialog::slotAddVerse);
164 m_verseRemoveButton = new QPushButton(tr("Remove Verse"), hbox );
165 hboxLayout->addWidget(m_verseRemoveButton);
166 connect(m_verseRemoveButton, &QAbstractButton::clicked, this, &LyricEditDialog::slotRemoveVerse);
167
168 // Avoid buttons to become exaggeratedly large when dialog is resized
169 QFrame *f = new QFrame( hbox );
170 hboxLayout->addWidget(f);
171 hbox->setLayout(hboxLayout);
172 hboxLayout->setStretchFactor(f, 10);
173
174 m_textEdit = new QTextEdit(groupBox);
175 groupBoxLayout->addWidget(m_textEdit);
176
177 m_textEdit->setMinimumWidth(300);
178 m_textEdit->setMinimumHeight(200);
179
180 m_currentVerse = 0;
181 unparse();
182 verseDialogRepopulate();
183
184 m_previousTexts = m_texts;
185 m_previousVerseCount = m_verseCount;
186
187 m_textEdit->setFocus();
188
189 groupBox->setLayout(groupBoxLayout);
190
191 QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok
192 | QDialogButtonBox::Cancel
193 | QDialogButtonBox::Help);
194 metagrid->addWidget(buttonBox, 1, 0);
195 metagrid->setRowStretch(0, 10);
196 connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
197 connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
198 connect(buttonBox, &QDialogButtonBox::helpRequested, this, &LyricEditDialog::slotHelpRequested);
199 }
200
201 void
showDescriptionOfSelectedSegment()202 LyricEditDialog::showDescriptionOfSelectedSegment()
203 {
204 // Get selected segment characteristics
205 Composition *comp = m_segment->getComposition();
206 timeT segStart = m_segment->getStartTime();
207 timeT segEnd = m_segment->getEndMarkerTime();
208 int barStart = comp->getBarNumber(segStart) + 1;
209 int barEnd = comp->getBarNumber(segEnd - 1) + 1;
210 QString label = strtoqstr(m_segment->getLabel());
211
212 // Shorten too long labels
213 if (label.length() > 53) label = label.left(50) + "...";
214
215 // Create the description (on two lines)
216 QString descr1 = QString(tr("Selected segment lays on track %1, bar %2 to %3"))
217 .arg(comp->getTrackPositionById(m_segment->getTrack()) + 1)
218 .arg(barStart)
219 .arg(barEnd);
220 QString descr2 = QString(tr("and is labeled \"%1\""))
221 .arg(label);
222
223 // Write out the two lines
224 m_descr1->setText(descr1);
225 m_descr2->setText(descr2);
226 }
227
228 void
slotSegmentChanged(QAction * a)229 LyricEditDialog::slotSegmentChanged(QAction * a)
230 {
231 Segment * newSeg= m_menuActionsMap[a];
232
233 // Do nothing if segment unchanged
234 if (m_segment == newSeg) return;
235
236 // Lyrics of current segment have been modified
237 // (1) if verse count has decreased
238 // (2) or if some preexisting verse have changed
239 // (3) or if verse count has increased and new verses are not skeletons
240
241 bool changed = false;
242 if (m_verseCount < m_previousVerseCount) {
243 changed = true; // (1)
244 } else {
245 for (int i = 0; i < m_verseCount; i++) {
246 if (i < m_previousVerseCount) {
247 if (m_previousTexts[i] != getLyricData(i)) {
248 changed = true; // (2)
249 break;
250 }
251 } else {
252 if (getLyricData(i) != m_skeleton) {
253 changed = true; // (3)
254 break;
255 }
256 }
257 }
258 }
259
260 // If lyrics have been modified, give the user a chance to keep the changes
261 if (changed) {
262 int okToChange = QMessageBox::warning( this, tr("Rosegarden - Warning"),
263 tr("<qt><p>The current segment lyrics have been modified.</p>"
264 "<p>The modifications will be lost if a new segment is selected.</p>"
265 "<p>Do you really want to select a new segment?</p></qt>"),
266 QMessageBox::Yes | QMessageBox::No, QMessageBox::No );
267
268 // Do nothing if user replied no
269 if (okToChange != QMessageBox::Yes) return;
270 }
271
272 m_segment = newSeg;
273 showDescriptionOfSelectedSegment();
274
275 m_texts.clear();
276 m_currentVerse = 0;
277 unparse();
278 verseDialogRepopulate();
279
280 m_previousTexts = m_texts;
281 m_previousVerseCount = m_verseCount;
282
283 m_textEdit->setFocus();
284 }
285
286 void
slotVerseNumberChanged(int verse)287 LyricEditDialog::slotVerseNumberChanged(int verse)
288 {
289 NOTATION_DEBUG << "LyricEditDialog::slotVerseNumberChanged(" << verse << ")";
290
291 QString text = m_textEdit->toPlainText();
292 m_texts[m_currentVerse] = text;
293 m_textEdit->setPlainText(m_texts[verse]);
294 m_currentVerse = verse;
295 }
296
297 void
slotAddVerse()298 LyricEditDialog::slotAddVerse()
299 {
300 NOTATION_DEBUG << "LyricEditDialog::slotAddVerse";
301
302 m_texts.push_back(m_skeleton);
303
304 m_verseCount++;
305
306 // NOTE slotVerseNumberChanged should be called with m_currentVerse argument
307 // if we ever decide to add new verse between existing ones
308 slotVerseNumberChanged(m_verseCount - 1);
309 verseDialogRepopulate();
310 }
311
312 void
slotRemoveVerse()313 LyricEditDialog::slotRemoveVerse()
314 {
315 NOTATION_DEBUG << "LyricEditDialog::slotRemoveVerse";
316
317 RG_DEBUG << "deleting at position " << m_currentVerse;
318 std::vector<QString>::iterator itr = m_texts.begin();
319 for (int i = 0; i < m_currentVerse; ++i) ++itr;
320
321 RG_DEBUG << "text being deleted is: " << *itr;
322 if (m_verseCount > 1) {
323 m_texts.erase(itr);
324 m_verseCount--;
325 if (m_currentVerse == m_verseCount) m_currentVerse--;
326 } else {
327 RG_DEBUG << "deleting last verse";
328 m_texts.clear();
329 m_texts.push_back(m_skeleton);
330 }
331 verseDialogRepopulate();
332 }
333
334 void
countVerses()335 LyricEditDialog::countVerses()
336 {
337 m_verseCount = m_segment->getVerseCount();
338
339 // If no verse, add an empty one to give a workplace to the user
340 // (else the user would need to press the "add verse" button)
341 if (m_verseCount == 0) m_verseCount = 1;
342 }
343
344 void
unparse()345 LyricEditDialog::unparse()
346 {
347 // This and SetLyricsCommand::execute() are opposites that will
348 // need to be kept in sync with any changes in one another. (They
349 // should really both be in a common lyric management class.)
350
351 countVerses();
352
353 Composition *comp = m_segment->getComposition();
354
355 bool firstNote = true;
356 timeT lastTime = m_segment->getStartTime();
357 int lastBarNo = comp->getBarNumber(lastTime);
358 std::map<int, bool> haveLyric;
359
360 QString fragment = QString("[%1] ").arg(lastBarNo + 1);
361
362 m_skeleton = fragment;
363 m_texts.clear();
364 for (int v = 0; v < m_verseCount; ++v) {
365 m_texts.push_back(fragment);
366 haveLyric[v] = false;
367 }
368
369 for (Segment::iterator i = m_segment->begin();
370 m_segment->isBeforeEndMarker(i); ++i) {
371
372 bool isNote = (*i)->isa(Note::EventType);
373 bool isLyric = false;
374
375 if (!isNote) {
376 if ((*i)->isa(Text::EventType)) {
377 std::string textType;
378 if ((*i)->get<String>(Text::TextTypePropertyName, textType) &&
379 textType == Text::Lyric) {
380 isLyric = true;
381 }
382 }
383 } else {
384 if ((*i)->has(BaseProperties::TIED_BACKWARD) &&
385 (*i)->get<Bool>(BaseProperties::TIED_BACKWARD)) {
386 continue;
387 }
388 }
389
390 if (!isNote && !isLyric) continue;
391
392 timeT myTime = (*i)->getNotationAbsoluteTime();
393 int myBarNo = comp->getBarNumber(myTime);
394
395 if (myBarNo > lastBarNo) {
396
397 fragment = "";
398
399 while (myBarNo > lastBarNo) {
400 fragment += " /";
401 ++lastBarNo;
402 }
403
404 fragment += QString("\n[%1] ").arg(myBarNo + 1);
405
406 m_skeleton += fragment;
407 for (int v = 0; v < m_verseCount; ++v) m_texts[v] += fragment;
408 }
409
410 if (isNote) {
411 if ((myTime > lastTime) || firstNote) {
412 m_skeleton += " .";
413 for (int v = 0; v < m_verseCount; ++v) {
414 if (!haveLyric[v]) m_texts[v] += " .";
415 haveLyric[v] = false;
416 }
417 lastTime = myTime;
418 firstNote = false;
419 }
420 }
421
422 if (isLyric) {
423
424 std::string ssyllable;
425 (*i)->get<String>(Text::TextPropertyName, ssyllable);
426
427 long verse = 0;
428 (*i)->get<Int>(Text::LyricVersePropertyName, verse);
429
430 QString syllable(strtoqstr(ssyllable));
431 syllable.replace(QRegularExpression("\\s+"), "~");
432
433 m_texts[verse] += " " + syllable;
434 haveLyric[verse] = true;
435 }
436 }
437
438 if (!m_texts.empty()) {
439 m_textEdit->setPlainText(m_texts[0]);
440 } else {
441 m_texts.push_back(m_skeleton);
442 }
443 }
444
445 int
getVerseCount() const446 LyricEditDialog::getVerseCount() const
447 {
448 return m_verseCount;
449 }
450
451 QString
getLyricData(int verse) const452 LyricEditDialog::getLyricData(int verse) const
453 {
454 if (verse == m_verseNumber->currentIndex()) {
455 return m_textEdit->toPlainText();
456 } else {
457 return m_texts[verse];
458 }
459 }
460
461 void
verseDialogRepopulate()462 LyricEditDialog::verseDialogRepopulate()
463 {
464 m_verseNumber->clear();
465
466 for (int i = 0; i < m_verseCount; ++i) {
467 m_verseNumber->addItem(tr("Verse %1").arg(i + 1));
468 }
469
470 if (m_verseCount == 12)
471 m_verseAddButton->setEnabled(false);
472 else
473 m_verseAddButton->setEnabled(true);
474
475 if (m_verseCount == 0)
476 m_verseRemoveButton->setEnabled(false);
477 else
478 m_verseRemoveButton->setEnabled(true);
479
480 m_verseNumber->setCurrentIndex(m_currentVerse);
481
482 RG_DEBUG << "m_currentVerse = " << m_currentVerse << ", text = " << m_texts[m_currentVerse];
483 m_textEdit->setPlainText(m_texts[m_currentVerse]);
484 }
485
486
487 void
slotHelpRequested()488 LyricEditDialog::slotHelpRequested()
489 {
490 // TRANSLATORS: if the manual is translated into your language, you can
491 // change the two-letter language code in this URL to point to your language
492 // version, eg. "http://rosegardenmusic.com/wiki/doc:lyricEditDialog-es" for the
493 // Spanish version. If your language doesn't yet have a translation, feel
494 // free to create one.
495 QString helpURL = tr("http://rosegardenmusic.com/wiki/doc:lyricEditDialog-en");
496 QDesktopServices::openUrl(QUrl(helpURL));
497 }
498 }
499