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 "TupletDialog.h"
20 #include <QLayout>
21 
22 #include "base/NotationTypes.h"
23 #include "gui/editors/notation/NotationStrings.h"
24 #include "gui/editors/notation/NotePixmapFactory.h"
25 #include <QComboBox>
26 #include <QDialog>
27 #include <QDialogButtonBox>
28 #include <QCheckBox>
29 #include <QFrame>
30 #include <QGridLayout>
31 #include <QGroupBox>
32 #include <QLabel>
33 #include <QObject>
34 #include <QString>
35 #include <QWidget>
36 #include <QVBoxLayout>
37 #include <QUrl>
38 #include <QDesktopServices>
39 
40 
41 namespace Rosegarden
42 {
43 
44 TupletDialog::TupletDialog(QWidget *parent, Note::Type defaultUnitType,
45                            timeT maxDuration) :
46         QDialog(parent),
47         m_maxDuration(maxDuration)
48 {
49     //setHelp("nv-tuplets");
50     setModal(true);
51     setWindowTitle(tr("Tuplet"));
52 
53     QGridLayout *metagrid = new QGridLayout;
54     setLayout(metagrid);
55     QWidget *vbox = new QWidget(this);
56     QVBoxLayout *vboxLayout = new QVBoxLayout;
57     metagrid->addWidget(vbox, 0, 0);
58 
59 
60     QGroupBox *timingBox = new QGroupBox( tr("New timing for tuplet group"), vbox );
61     timingBox->setContentsMargins(5, 5, 5, 5);
62     QGridLayout *timingLayout = new QGridLayout(timingBox);
63     timingLayout->setSpacing(5);
64     vboxLayout->addWidget(timingBox);
65 
66     if (m_maxDuration > 0) {
67 
68         // bit of a sanity check
69         if (maxDuration < Note(Note::Semiquaver).getDuration()) {
70             maxDuration = Note(Note::Semiquaver).getDuration();
71         }
72 
73         Note::Type maxUnitType =
74             Note::getNearestNote(maxDuration / 2, 0).getNoteType();
75         if (defaultUnitType > maxUnitType)
76             defaultUnitType = maxUnitType;
77     }
78 
79     int timingRow = 0;
80 
81     m_hasTimingAlready = new QCheckBox
82         (tr("Timing is already correct: update display only"), timingBox);
83     m_hasTimingAlready->setChecked(false);
84     timingLayout->addWidget(m_hasTimingAlready, timingRow, 0, 1, 3);
85 
86     timingLayout->addWidget(new QLabel(tr("Play "), timingBox), ++timingRow, 0);
87 
88     m_untupledCombo = new QComboBox(timingBox);
89     timingLayout->addWidget(m_untupledCombo, timingRow, 1);
90 
91     m_unitCombo = new QComboBox(timingBox);
92     timingLayout->addWidget(m_unitCombo, timingRow, 2);
93 
94     for (Note::Type t = Note::Shortest; t <= Note::Longest; ++t) {
95         Note note(t);
96         timeT duration(note.getDuration());
97         if (maxDuration > 0 && (2 * duration > maxDuration))
98             break;
99         timeT e; // error factor, ignore
100         m_unitCombo->addItem(NotePixmapFactory::makeNoteMenuPixmap(duration, e),
101                              NotationStrings::makeNoteMenuLabel(duration, false, e, true));
102         if (defaultUnitType == t) {
103             m_unitCombo->setCurrentIndex(m_unitCombo->count() - 1);
104         }
105     }
106 
107     timingLayout->addWidget(new QLabel(tr("in the time of  "), timingBox), ++timingRow, 0);
108 
109     m_tupledCombo = new QComboBox(timingBox);
110     timingLayout->addWidget(m_tupledCombo, timingRow, 1);
111 
112     timingBox->setLayout(timingLayout);
113 
114     connect(m_hasTimingAlready, &QAbstractButton::clicked, this, &TupletDialog::slotHasTimingChanged);
115 
116     updateUntupledCombo();
117     updateTupledCombo();
118 
119     m_timingDisplayGrid = new QGroupBox( tr("Timing calculations"), vbox );
120     QGridLayout *timingDisplayLayout = new QGridLayout(m_timingDisplayGrid);
121     vboxLayout->addWidget(m_timingDisplayGrid);
122 
123     int row = 0;
124 
125     if (maxDuration > 0) {
126 
127         timingDisplayLayout->addWidget(new QLabel(tr("Selected region:")),
128                                          row, 0);
129         timingDisplayLayout->addWidget(new QLabel(""), row, 1);
130         m_selectionDurationDisplay = new QLabel("x");
131         timingDisplayLayout->addWidget(m_selectionDurationDisplay, row, 2);
132         m_selectionDurationDisplay->setAlignment(Qt::AlignVCenter |
133                 Qt::AlignRight);
134         row++;
135     } else {
136         m_selectionDurationDisplay = nullptr;
137     }
138 
139     timingDisplayLayout->addWidget(new QLabel(tr("Group with current timing:")),
140                                      row, 0);
141     m_untupledDurationCalculationDisplay = new QLabel("x");
142     timingDisplayLayout->addWidget(m_untupledDurationCalculationDisplay,
143                                      row, 1);
144     m_untupledDurationDisplay = new QLabel("x");
145     m_untupledDurationDisplay->setAlignment(Qt::AlignVCenter |
146                                             Qt::AlignRight);
147     timingDisplayLayout->addWidget(m_untupledDurationDisplay, row, 2);
148     row++;
149 
150     timingDisplayLayout->addWidget(new QLabel(tr("Group with new timing:")),
151                                      row, 0);
152     m_tupledDurationCalculationDisplay = new QLabel("x");
153     timingDisplayLayout->addWidget(m_tupledDurationCalculationDisplay,
154                                      row, 1);
155     m_tupledDurationDisplay = new QLabel("x");
156     m_tupledDurationDisplay->setAlignment(Qt::AlignVCenter |
157                                           Qt::AlignRight);
158     timingDisplayLayout->addWidget(m_tupledDurationDisplay, row, 2);
159     row++;
160 
161     timingDisplayLayout->addWidget(new QLabel(tr("Gap created by timing change:")),
162                                      row, 0);
163     m_newGapDurationCalculationDisplay = new QLabel("x");
164     timingDisplayLayout->addWidget(m_newGapDurationCalculationDisplay,
165                                      row, 1);
166     m_newGapDurationDisplay = new QLabel("x");
167     m_newGapDurationDisplay->setAlignment(Qt::AlignVCenter |
168                                           Qt::AlignRight);
169     timingDisplayLayout->addWidget(m_newGapDurationDisplay, row, 2);
170     row++;
171 
172     if (maxDuration > 0) {
173 
174         timingDisplayLayout->addWidget(new QLabel(tr("Unchanged at end of selection:")),
175                                          row, 0);
176         m_unchangedDurationCalculationDisplay = new QLabel("x");
177         timingDisplayLayout->addWidget(m_unchangedDurationCalculationDisplay,
178                                          row, 1);
179         m_unchangedDurationDisplay = new QLabel("x");
180         m_unchangedDurationDisplay->setAlignment(Qt::AlignVCenter |
181                                                  Qt::AlignRight);
182         timingDisplayLayout->addWidget(m_unchangedDurationDisplay, row, 2);
183 
184     } else {
185         m_unchangedDurationDisplay = nullptr;
186     }
187 
188     m_timingDisplayGrid->setLayout(timingDisplayLayout);
189 
190     vbox->setLayout(vboxLayout);
191 
192     updateTimingDisplays();
193 
194     QObject::connect(m_unitCombo, SIGNAL(activated(const QString &)),
195                      this, SLOT(slotUnitChanged(const QString &)));
196 
197     QObject::connect(m_untupledCombo, SIGNAL(activated(const QString &)),
198                      this, SLOT(slotUntupledChanged(const QString &)));
199     QObject::connect(m_untupledCombo, SIGNAL(textChanged(const QString &)),
200                      this, SLOT(slotUntupledChanged(const QString &)));
201 
202     QObject::connect(m_tupledCombo, SIGNAL(activated(const QString &)),
203                      this, SLOT(slotTupledChanged(const QString &)));
204     QObject::connect(m_tupledCombo, SIGNAL(textChanged(const QString &)),
205                      this, SLOT(slotTupledChanged(const QString &)));
206     QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Help);
207     metagrid->addWidget(buttonBox, 1, 0);
208     metagrid->setRowStretch(0, 10);
209     connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
210     connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
211     connect(buttonBox, &QDialogButtonBox::helpRequested, this, &TupletDialog::slotHelpRequested);
212 }
213 
214 void
215 TupletDialog::slotHasTimingChanged()
216 {
217     updateUntupledCombo();
218     updateTupledCombo();
219     m_timingDisplayGrid->setEnabled(!m_hasTimingAlready->isChecked());
220 }
221 
222 Note::Type
223 TupletDialog::getUnitType() const
224 {
225     return Note::Shortest + m_unitCombo->currentIndex();
226 }
227 
228 int
229 TupletDialog::getUntupledCount() const
230 {
231     bool isNumeric = true;
232     int count = m_untupledCombo->currentText().toInt(&isNumeric);
233     if (count == 0 || !isNumeric)
234         return 1;
235     else
236         return count;
237 }
238 
239 int
240 TupletDialog::getTupledCount() const
241 {
242     bool isNumeric = true;
243     int count = m_tupledCombo->currentText().toInt(&isNumeric);
244     if (count == 0 || !isNumeric)
245         return 1;
246     else
247         return count;
248 }
249 
250 bool
251 TupletDialog::hasTimingAlready() const
252 {
253     return m_hasTimingAlready->isChecked();
254 }
255 
256 void
257 TupletDialog::updateUntupledCombo()
258 {
259     // Untupled combo can contain numbers up to the maximum
260     // duration divided by the unit duration.  If there's no
261     // maximum, we'll have to put in some likely values and
262     // allow the user to edit it.  Both the numerical combos
263     // should possibly be spinboxes, except I think I like
264     // being able to "suggest" a few values
265 
266     int maximum = 12;
267 
268     if (m_maxDuration) {
269         if (m_hasTimingAlready->isChecked()) {
270             maximum = (m_maxDuration * 2) / Note(getUnitType()).getDuration();
271         } else {
272             maximum = m_maxDuration / Note(getUnitType()).getDuration();
273         }
274     }
275 
276     QString previousText = m_untupledCombo->currentText();
277     if (previousText.toInt() == 0) {
278         if (maximum < 3)
279             previousText = QString("%1").arg(maximum);
280         else
281             previousText = "3";
282     }
283 
284     m_untupledCombo->clear();
285     bool setText = false;
286 
287     for (int i = 1; i <= maximum; ++i) {
288         QString text = QString("%1").arg(i);
289         m_untupledCombo->addItem(text);
290         if (m_hasTimingAlready->isChecked()) {
291             if (i == (m_maxDuration * 3) / (Note(getUnitType()).getDuration()*2)) {
292                 m_untupledCombo->setCurrentIndex(m_untupledCombo->count() - 1);
293             }
294         } else if (text == previousText) {
295             m_untupledCombo->setCurrentIndex(m_untupledCombo->count() - 1);
296             setText = true;
297         }
298     }
299 
300     if (!setText) {
301         m_untupledCombo->setEditText(previousText);
302     }
303 }
304 
305 void
306 TupletDialog::updateTupledCombo()
307 {
308     // should contain all positive integers less than the
309     // largest value in the untupled combo.  In principle
310     // we can support values larger, but we can't quite
311     // do the tupleting transformation yet
312 
313     int untupled = getUntupledCount();
314 
315     QString previousText = m_tupledCombo->currentText();
316     if (previousText.toInt() == 0 ||
317             previousText.toInt() > untupled) {
318         if (untupled < 2)
319             previousText = QString("%1").arg(untupled);
320         else
321             previousText = "2";
322     }
323 
324     m_tupledCombo->clear();
325 
326     for (int i = 1; i < untupled; ++i) {
327         QString text = QString("%1").arg(i);
328         m_tupledCombo->addItem(text);
329         if (m_hasTimingAlready->isChecked()) {
330             if (i == m_maxDuration / Note(getUnitType()).getDuration()) {
331                 m_tupledCombo->setCurrentIndex(m_tupledCombo->count() - 1);
332             }
333         } else if (text == previousText) {
334             m_tupledCombo->setCurrentIndex(m_tupledCombo->count() - 1);
335         }
336     }
337 }
338 
339 void
340 TupletDialog::updateTimingDisplays()
341 {
342     timeT unitDuration = Note(getUnitType()).getDuration();
343 
344     int untupledCount = getUntupledCount();
345     int tupledCount = getTupledCount();
346 
347     timeT untupledDuration = unitDuration * untupledCount;
348     timeT tupledDuration = unitDuration * tupledCount;
349 
350     if (m_selectionDurationDisplay) {
351         m_selectionDurationDisplay->setText(QString("%1").arg(m_maxDuration));
352     }
353 
354     m_untupledDurationCalculationDisplay->setText
355     (QString("  %1 x %2 = ").arg(untupledCount).arg(unitDuration));
356     m_untupledDurationDisplay->setText
357     (QString("%1").arg(untupledDuration));
358 
359     m_tupledDurationCalculationDisplay->setText
360     (QString("  %1 x %2 = ").arg(tupledCount).arg(unitDuration));
361     m_tupledDurationDisplay->setText
362     (QString("%1").arg(tupledDuration));
363 
364     m_newGapDurationCalculationDisplay->setText
365     (QString("  %1 - %2 = ").arg(untupledDuration).arg(tupledDuration));
366     m_newGapDurationDisplay->setText
367     (QString("%1").arg(untupledDuration - tupledDuration));
368 
369     if (m_selectionDurationDisplay && m_unchangedDurationDisplay) {
370         if (m_maxDuration != untupledDuration) {
371             m_unchangedDurationCalculationDisplay->setText
372             (QString("  %1 - %2 = ").arg(m_maxDuration).arg(untupledDuration));
373         } else {
374             m_unchangedDurationCalculationDisplay->setText("");
375         }
376         m_unchangedDurationDisplay->setText
377         (QString("%1").arg(m_maxDuration - untupledDuration));
378     }
379 }
380 
381 void
382 TupletDialog::slotUnitChanged(const QString &)
383 {
384     updateUntupledCombo();
385     updateTupledCombo();
386     updateTimingDisplays();
387 }
388 
389 void
390 TupletDialog::slotUntupledChanged(const QString &)
391 {
392     updateTupledCombo();
393     updateTimingDisplays();
394 }
395 
396 void
397 TupletDialog::slotTupledChanged(const QString &)
398 {
399     updateTimingDisplays();
400 }
401 
402 
403 void
404 TupletDialog::slotHelpRequested()
405 {
406     // TRANSLATORS: if the manual is translated into your language, you can
407     // change the two-letter language code in this URL to point to your language
408     // version, eg. "http://rosegardenmusic.com/wiki/doc:tupletDialog-es" for the
409     // Spanish version. If your language doesn't yet have a translation, feel
410     // free to create one.
411     QString helpURL = tr("http://rosegardenmusic.com/wiki/doc:tupletDialog-en");
412     QDesktopServices::openUrl(QUrl(helpURL));
413 }
414 }
415