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