1 /* TIATracker, (c) 2016 Andre "Kylearan" Wichmann.
2  * Website: https://bitbucket.org/kylearan/tiatracker
3  * Email: andre.wichmann@gmx.de
4  * See the file "license.txt" for information on usage and redistribution
5  * of this file.
6  */
7 
8 #include "patterneditor.h"
9 #include <QFontMetrics>
10 #include <QPainter>
11 #include "mainwindow.h"
12 #include "track/pattern.h"
13 #include "track/sequence.h"
14 #include "track/sequenceentry.h"
15 #include "track/note.h"
16 #include "tiasound/pitchguidefactory.h"
17 #include "tiasound/pitchguide.h"
18 #include "tiasound/instrumentpitchguide.h"
19 #include "tiasound/tiasound.h"
20 #include <QWheelEvent>
21 
22 
PatternEditor(QWidget * parent)23 PatternEditor::PatternEditor(QWidget *parent) : QWidget(parent)
24 {
25     legendFont.setPixelSize(legendFontSize);
26     QFontMetrics legendFontMetrics(legendFont);
27     legendFontHeight = legendFontMetrics.height();
28     timeAreaWidth = legendFontMetrics.width("000:00");
29 
30     noteFont.setPixelSize(noteFontSize);
31     QFontMetrics noteFontMetrics(noteFont);
32     noteFontHeight = noteFontMetrics.height();
33     noteAreaWidth = noteFontMetrics.width("000: C#4 I7 31")
34             + 2*noteMargin;
35 
36     widgetWidth = 2*patternNameWidth
37             + 2*noteAreaWidth
38             + timeAreaWidth;
39     setFixedWidth(widgetWidth);
40 
41     setFocusPolicy(Qt::StrongFocus);
42 }
43 
44 /*************************************************************************/
45 
registerTrack(Track::Track * newTrack)46 void PatternEditor::registerTrack(Track::Track *newTrack) {
47     pTrack = newTrack;
48 }
49 
50 /*************************************************************************/
51 
registerPitchGuide(TiaSound::PitchGuide * newGuide)52 void PatternEditor::registerPitchGuide(TiaSound::PitchGuide *newGuide) {
53     pPitchGuide = newGuide;
54 }
55 
56 /*************************************************************************/
57 
registerPlayer(Emulation::Player * newPlayer)58 void PatternEditor::registerPlayer(Emulation::Player *newPlayer) {
59     pPlayer = newPlayer;
60 }
61 
62 /*************************************************************************/
63 
registerMuteAction(QAction * newAction)64 void PatternEditor::registerMuteAction(QAction *newAction) {
65     muteAction = newAction;
66 }
67 
68 /*************************************************************************/
69 
registerPatternMenu(QMenu * newPatternMenu)70 void PatternEditor::registerPatternMenu(QMenu *newPatternMenu) {
71     pPatternMenu = newPatternMenu;
72 }
73 
74 /*************************************************************************/
75 
registerChannelMenu(QMenu * newChannelMenu)76 void PatternEditor::registerChannelMenu(QMenu *newChannelMenu) {
77     pChannelMenu = newChannelMenu;
78 }
79 
80 /*************************************************************************/
81 
registerInstrumentSelector(InstrumentSelector * selector)82 void PatternEditor::registerInstrumentSelector(InstrumentSelector *selector) {
83     pInsSelector = selector;
84 }
85 
86 /*************************************************************************/
87 
setEditPos(int newPos)88 void PatternEditor::setEditPos(int newPos) {
89     editPos = newPos;
90     if (editPos < 0) {
91         editPos = 0;
92     }
93     if (editPos >= pTrack->getChannelNumRows(selectedChannel)) {
94         editPos = pTrack->getChannelNumRows(selectedChannel) - 1;
95     }
96     emit editPosChanged(editPos);
97     emit channelContextEvent(selectedChannel, editPos);
98     update();
99 }
100 
101 /*************************************************************************/
102 
setEditPos(int newChannel,int newPos)103 void PatternEditor::setEditPos(int newChannel, int newPos) {
104     selectedChannel = newChannel;
105     setEditPos(newPos);
106 }
107 
108 /*************************************************************************/
109 
validateEditPos()110 void PatternEditor::validateEditPos() {
111     setEditPos(editPos);
112 }
113 
114 /*************************************************************************/
115 
advanceEditPos()116 void PatternEditor::advanceEditPos() {
117     setEditPos(editPos + 1);
118 }
119 
120 /*************************************************************************/
121 
setRowsPerBeat(int value)122 void PatternEditor::setRowsPerBeat(int value) {
123     pTrack->rowsPerBeat = value;
124     update();
125 }
126 
127 /*************************************************************************/
128 
setRowToInstrument(int frequency)129 void PatternEditor::setRowToInstrument(int frequency) {
130     int patternIndex = pTrack->getPatternIndex(selectedChannel, editPos);
131     int noteIndex = pTrack->getNoteIndexInPattern(selectedChannel, editPos);
132     int instrumentIndex = pInsSelector->getSelectedInstrument();
133     if (instrumentIndex < 7) {
134         // Meldodic instrument
135         pTrack->patterns[patternIndex].notes[noteIndex].type = Track::Note::instrumentType::Instrument;
136         pTrack->patterns[patternIndex].notes[noteIndex].instrumentNumber = instrumentIndex;
137         pTrack->patterns[patternIndex].notes[noteIndex].value = frequency;
138     } else {
139         // Percussion instrument
140         pTrack->patterns[patternIndex].notes[noteIndex].type = Track::Note::instrumentType::Percussion;
141         pTrack->patterns[patternIndex].notes[noteIndex].instrumentNumber = instrumentIndex - Track::Track::numInstruments;
142     }
143     advanceEditPos();
144     update();
145 }
146 
147 /*************************************************************************/
148 
newPlayerPos(int pos1,int pos2)149 void PatternEditor::newPlayerPos(int pos1, int pos2) {
150     if (follow) {
151         if (selectedChannel == 0) {
152             setEditPos(pos1);
153         } else {
154             setEditPos(pos2);
155         }
156     }
157 }
158 
159 /*************************************************************************/
160 
toggleFollow_clicked(bool toggle)161 void PatternEditor::toggleFollow_clicked(bool toggle) {
162     follow = toggle;
163 }
164 
165 /*************************************************************************/
166 
toggleLoop_clicked(bool toggle)167 void PatternEditor::toggleLoop_clicked(bool toggle) {
168     loop = toggle;
169 }
170 
171 /*************************************************************************/
172 
getEditPos()173 int PatternEditor::getEditPos() {
174     return editPos;
175 }
176 
177 /*************************************************************************/
178 
getSelectedChannel()179 int PatternEditor::getSelectedChannel() {
180     return selectedChannel;
181 }
182 
183 /*************************************************************************/
184 
sizeHint() const185 QSize PatternEditor::sizeHint() const {
186     return QSize(widgetWidth, minHeight);
187 }
188 
189 /*************************************************************************/
190 
moveUp(bool)191 void PatternEditor::moveUp(bool) {
192     setEditPos(editPos - 1);
193 }
194 
195 /*************************************************************************/
196 
moveDown(bool)197 void PatternEditor::moveDown(bool) {
198     setEditPos(editPos + 1);
199 }
200 
201 /*************************************************************************/
202 
moveLeft(bool)203 void PatternEditor::moveLeft(bool) {
204     selectedChannel = 0;
205     setEditPos(editPos);
206     emit editChannelChanged(selectedChannel);
207 }
208 
209 /*************************************************************************/
210 
moveRight(bool)211 void PatternEditor::moveRight(bool) {
212     selectedChannel = 1;
213     setEditPos(editPos);
214     emit editChannelChanged(selectedChannel);
215 }
216 
217 /*************************************************************************/
218 
switchChannel(bool)219 void PatternEditor::switchChannel(bool) {
220     selectedChannel = 1 - selectedChannel;
221     setEditPos(editPos);
222     emit editChannelChanged(selectedChannel);
223 }
224 
225 /*************************************************************************/
226 
gotoFirstRow(bool)227 void PatternEditor::gotoFirstRow(bool) {
228     setEditPos(0);
229 }
230 
231 /*************************************************************************/
232 
gotoLastRow(bool)233 void PatternEditor::gotoLastRow(bool) {
234     setEditPos(pTrack->getChannelNumRows(selectedChannel) - 1);
235 }
236 
237 /*************************************************************************/
238 
gotoNextPattern(bool)239 void PatternEditor::gotoNextPattern(bool) {
240     int newEntryIndex = pTrack->getSequenceEntryIndex(selectedChannel, editPos) + 1;
241     if (newEntryIndex == pTrack->channelSequences[selectedChannel].sequence.size()) {
242         newEntryIndex--;
243     }
244     int newPos = pTrack->channelSequences[selectedChannel].sequence[newEntryIndex].firstNoteNumber;
245     setEditPos(newPos);
246 }
247 
248 /*************************************************************************/
249 
gotoPreviousPattern(bool)250 void PatternEditor::gotoPreviousPattern(bool) {
251     int newEntryIndex = pTrack->getSequenceEntryIndex(selectedChannel, editPos) - 1;
252     if (newEntryIndex < 0) {
253         newEntryIndex = 0;
254     }
255     int newPos = pTrack->channelSequences[selectedChannel].sequence[newEntryIndex].firstNoteNumber;
256     setEditPos(newPos);
257 }
258 
259 /*************************************************************************/
260 
constructRowString(int curPatternNoteIndex,Track::Pattern * curPattern)261 QString PatternEditor::constructRowString(int curPatternNoteIndex, Track::Pattern *curPattern) {
262     QString rowText = QString::number(curPatternNoteIndex + 1);
263     if (curPatternNoteIndex + 1 < 10) {
264         rowText.prepend("  ");
265     } else if (curPatternNoteIndex + 1 < 100) {
266         rowText.prepend(" ");
267     }
268     switch (curPattern->notes[curPatternNoteIndex].type) {
269     case Track::Note::instrumentType::Hold:
270         rowText.append(":    |");
271         break;
272     case Track::Note::instrumentType::Slide: {
273         int frequency = curPattern->notes[curPatternNoteIndex].value;
274         rowText.append(":   ");
275         rowText.append("  SL");
276         // Frequency change
277         if (frequency < 0) {
278             rowText.append(" ");
279         } else {
280             rowText.append(" +");
281         }
282         rowText.append(QString::number(frequency));
283         break;
284     }
285     case Track::Note::instrumentType::Pause:
286         rowText.append(":   ---");
287         break;
288     case Track::Note::instrumentType::Percussion: {
289         int percNum = curPattern->notes[curPatternNoteIndex].instrumentNumber + 1;
290         if (percNum < 10) {
291             rowText.append(":   P ");
292         } else {
293             rowText.append(":   P");
294         }
295         rowText.append(QString::number(percNum));
296         break;
297     }
298     case Track::Note::instrumentType::Instrument: {
299         int insNum = curPattern->notes[curPatternNoteIndex].instrumentNumber;
300         // Pitch
301         int frequency = curPattern->notes[curPatternNoteIndex].value;
302         TiaSound::Distortion dist = pTrack->instruments[insNum].baseDistortion;
303         // In case the instrument got changed from PURE_COMBINED to something else
304         if (frequency > 31 && dist != TiaSound::Distortion::PURE_COMBINED) {
305             frequency -= 32;
306         }
307         TiaSound::InstrumentPitchGuide *pIPG = &(pPitchGuide->instrumentGuides[dist]);
308         TiaSound::Note note = pIPG->getNote(frequency);
309         if (note == TiaSound::Note::NotANote) {
310             rowText.append(": ???");
311         } else {
312             rowText.append(": ");
313             rowText.append(TiaSound::getNoteNameWithOctaveFixedWidth(note));
314         }
315         // Instrument number
316         rowText.append(" I");
317         rowText.append(QString::number(insNum + 1));
318         // Frequency
319         if (frequency < 10) {
320             rowText.append("  ");
321         } else {
322             rowText.append(" ");
323         }
324         rowText.append(QString::number(frequency));
325         break;
326     }
327     default:
328         rowText.append(": ??? ");
329         break;
330     }
331 
332     return rowText;
333 }
334 
drawPatternNameAndSeparator(int yPos,int nameXPos,int curPatternNoteIndex,int channel,int xPos,int curEntryIndex,QPainter * painter,Track::Pattern * curPattern)335 void PatternEditor::drawPatternNameAndSeparator(int yPos, int nameXPos, int curPatternNoteIndex, int channel, int xPos, int curEntryIndex, QPainter *painter, Track::Pattern *curPattern)
336 {
337     if (curPatternNoteIndex == 0) {
338         painter->fillRect(xPos - noteMargin, yPos, noteAreaWidth, 1, MainWindow::contentDarker);
339         painter->setFont(legendFont);
340         painter->setPen(MainWindow::contentDarker);
341         int alignment = channel == 0 ? Qt::AlignRight : Qt::AlignLeft;
342         QString patternName = QString::number(curEntryIndex + 1);
343         patternName.append(": ");
344         patternName.append(curPattern->name);
345         if (!pTrack->globalSpeed && channel == 0) {
346             patternName.append(" (");
347             patternName.append(QString::number(curPattern->evenSpeed));
348             patternName.append("/");
349             patternName.append(QString::number(curPattern->oddSpeed));
350             patternName.append(")");
351         }
352         if (curEntryIndex == pTrack->startPatterns[channel]) {
353             painter->setPen(MainWindow::green);
354 
355         } else {
356             painter->setPen(MainWindow::blue);
357         }
358         painter->drawText(nameXPos, yPos, patternNameWidth - 2*patternNameMargin, legendFontHeight, alignment, patternName);
359     }
360 }
361 
drawGoto(int channel,int yPos,Track::Pattern * curPattern,Track::SequenceEntry * curEntry,QPainter * painter,int nameXPos,int curPatternNoteIndex)362 void PatternEditor::drawGoto(int channel, int yPos, Track::Pattern *curPattern, Track::SequenceEntry *curEntry, QPainter *painter, int nameXPos, int curPatternNoteIndex)
363 {
364     if (curPatternNoteIndex == curPattern->notes.size() - 1
365             && curEntry->gotoTarget != -1) {
366         int alignment = channel == 0 ? Qt::AlignRight : Qt::AlignLeft;
367         painter->setFont(legendFont);
368         if (curEntry->gotoTarget < 128) {
369             painter->setPen(MainWindow::blue);
370         } else {
371             painter->setPen(MainWindow::red);
372         }
373         painter->drawText(nameXPos, yPos, patternNameWidth - 2*patternNameMargin, legendFontHeight, alignment,
374                           "GOTO " + QString::number(curEntry->gotoTarget + 1));
375     }
376 }
377 
drawTimestamp(int row,QPainter * painter,int yPos,int channel)378 void PatternEditor::drawTimestamp(int row, QPainter *painter, int yPos, int channel)
379 {
380     if (pTrack->globalSpeed) {
381         int ticksPerSecond = pTrack->getTvMode() == TiaSound::TvStandard::PAL ? 50 : 60;
382         long numOddTicks = int((row + 1)/2)*pTrack->oddSpeed;
383         long numEvenTicks = int(row/2)*pTrack->evenSpeed;
384         long numTick = numOddTicks + numEvenTicks;
385         int curTicks = row%2 == 0 ? pTrack->evenSpeed : pTrack->oddSpeed;
386         if (channel == 0 && numTick%ticksPerSecond < curTicks) {
387             int minute = numTick/(ticksPerSecond*60);
388             int second = (numTick%(ticksPerSecond*60))/ticksPerSecond;
389             QString timestampText = QString::number(minute);
390             if (second < 10) {
391                 timestampText.append(":0");
392             } else {
393                 timestampText.append(":");
394             }
395             timestampText.append(QString::number(second));
396             painter->setFont(legendFont);
397             painter->setPen(MainWindow::contentDarker);
398             painter->drawText(patternNameWidth + noteAreaWidth, yPos, timeAreaWidth, legendFontHeight, Qt::AlignHCenter, timestampText);
399         }
400     }
401 }
402 
paintChannel(QPainter * painter,int channel,int xPos,int nameXPos)403 void PatternEditor::paintChannel(QPainter *painter, int channel, int xPos, int nameXPos) {
404     // Calc first note/pattern
405     int firstNoteIndex = max(0, editPos - numRows/2);
406     // Don't do anything if we are behind the last note
407     int channelSize = pTrack->getChannelNumRows(channel);
408     if (firstNoteIndex >= channelSize) {
409         return;
410     }
411     // Get pointers to first note to paint
412     int curEntryIndex = 0;
413     Track::SequenceEntry *curEntry = &(pTrack->channelSequences[channel].sequence[0]);
414     Track::Pattern *curPattern = &(pTrack->patterns[curEntry->patternIndex]);
415     while (firstNoteIndex >= curEntry->firstNoteNumber + curPattern->notes.size()) {
416         curEntryIndex++;
417         curEntry = &(pTrack->channelSequences[channel].sequence[curEntryIndex]);
418         curPattern = &(pTrack->patterns[curEntry->patternIndex]);
419     }
420     int curPatternNoteIndex = firstNoteIndex - curEntry->firstNoteNumber;
421     // Draw rows
422     for (int row = firstNoteIndex; row <= editPos + numRows/2; ++row) {
423         int yPos = topMargin + noteFontHeight*(row - (editPos - numRows/2));
424         // First row in beat?
425         if (row%(pTrack->rowsPerBeat) == 0 && (channel != selectedChannel || row != editPos)) {
426             painter->fillRect(xPos - noteMargin, yPos, noteAreaWidth, noteFontHeight, MainWindow::darkHighlighted);
427         }
428         QString rowText = constructRowString(curPatternNoteIndex, curPattern);
429         painter->setFont(noteFont);
430         painter->setPen(MainWindow::blue);
431         painter->drawText(xPos, yPos, noteAreaWidth - 2*noteMargin, noteFontHeight, Qt::AlignLeft, rowText);
432 
433         drawPatternNameAndSeparator(yPos, nameXPos, curPatternNoteIndex, channel, xPos, curEntryIndex, painter, curPattern);
434         drawGoto(channel, yPos, curPattern, curEntry, painter, nameXPos, curPatternNoteIndex);
435         drawTimestamp(row, painter, yPos, channel);
436 
437         // Advance note
438         if (!pTrack->getNextNote(channel, &curEntryIndex, &curPatternNoteIndex)) {
439             // End of track reached: Stop drawing
440             break;
441         }
442         curEntry = &(pTrack->channelSequences[channel].sequence[curEntryIndex]);
443         curPattern = &(pTrack->patterns[curEntry->patternIndex]);
444     }
445 }
446 
paintEvent(QPaintEvent *)447 void PatternEditor::paintEvent(QPaintEvent *) {
448     QPainter painter(this);
449 
450     // Pattern name areas
451     painter.fillRect(0, 0, patternNameWidth, height(), MainWindow::lightHighlighted);
452     painter.fillRect(widgetWidth - patternNameWidth, 0, patternNameWidth, height(), MainWindow::lightHighlighted);
453     // Note areas
454     painter.fillRect(patternNameWidth, 0, noteAreaWidth, height(), MainWindow::dark);
455     painter.fillRect(patternNameWidth + noteAreaWidth + timeAreaWidth, 0, noteAreaWidth, height(), MainWindow::dark);
456     // Time area
457     painter.fillRect(patternNameWidth + noteAreaWidth, 0, timeAreaWidth, height(), MainWindow::lightHighlighted);
458     // Current highlights
459     int highlightY = height()/2 - noteFontHeight/2;
460     int highlightX = patternNameWidth + selectedChannel*(noteAreaWidth + timeAreaWidth);
461     painter.fillRect(highlightX, highlightY, noteAreaWidth, noteFontHeight, MainWindow::light);
462 
463     // Calc number of visible rows
464     numRows = height()/noteFontHeight;
465     if (numRows%2 == 0) {
466         numRows--;
467     }
468     topMargin = (height() - numRows*noteFontHeight)/2;
469 
470     // Paint channels
471     paintChannel(&painter, 0, patternNameWidth + noteMargin, patternNameMargin);
472     paintChannel(&painter, 1, patternNameWidth + noteAreaWidth + timeAreaWidth + noteMargin, width() - patternNameWidth + patternNameMargin);
473 
474 }
475 
476 /*************************************************************************/
477 
wheelEvent(QWheelEvent * event)478 void PatternEditor::wheelEvent(QWheelEvent *event) {
479     int newPos = editPos - event->delta()/100;
480     setEditPos(newPos);
481 }
482 
483 /*************************************************************************/
484 
clickedInValidRow(int x,int y,int * channel,int * noteIndex)485 bool PatternEditor::clickedInValidRow(int x, int y, int *channel, int *noteIndex) {
486     // Do nothing if we are outside a valid row
487     if (y < topMargin || y > topMargin + numRows*noteFontHeight) {
488         return false;
489     }
490     int row = (y - topMargin)/noteFontHeight;
491     *noteIndex = editPos - (numRows/2 - row);
492     if (x < patternNameWidth + noteAreaWidth) {
493         *channel = 0;
494     } else  if (x >= patternNameWidth + noteAreaWidth + timeAreaWidth) {
495         *channel = 1;
496     } else {
497         return false;
498     }
499     int channelSize = pTrack->getChannelNumRows(*channel);
500     if (*noteIndex < 0 || *noteIndex >= channelSize) {
501         return false;
502     }
503     return true;
504 }
505 
506 
507 /*************************************************************************/
508 
mousePressEvent(QMouseEvent * event)509 void PatternEditor::mousePressEvent(QMouseEvent *event) {
510     if (event->button() != Qt::LeftButton) {
511         return;
512     }
513     int channel;
514     int noteIndex;
515     if (!clickedInValidRow(event->x(), event->y(), &channel, &noteIndex)) {
516         return;
517     }
518     selectedChannel = channel;
519     setEditPos(noteIndex);
520     emit editChannelChanged(selectedChannel);
521 }
522 
523 /*************************************************************************/
524 
contextMenuEvent(QContextMenuEvent * event)525 void PatternEditor::contextMenuEvent(QContextMenuEvent *event) {
526     int channel;
527     int noteIndex;
528     if (!clickedInValidRow(event->x(), event->y(), &channel, &noteIndex)) {
529         return;
530     }
531 
532     // Set correct mute toggle state
533     muteAction->setChecked(pPlayer->channelMuted[channel]);
534     // Determine correct context menu to display
535     if (event->x() < patternNameWidth) {
536         emit channelContextEvent(channel, noteIndex);
537         pPatternMenu->exec(event->globalPos());
538     } else if (event->x() >= patternNameWidth && event->x() < patternNameWidth + noteAreaWidth) {
539         emit channelContextEvent(channel, noteIndex);
540         pChannelMenu->exec(event->globalPos());
541     } else if (event->x() >= patternNameWidth + noteAreaWidth + timeAreaWidth
542                && event->x() < patternNameWidth + noteAreaWidth + timeAreaWidth + noteAreaWidth) {
543         emit channelContextEvent(channel, noteIndex);
544         pChannelMenu->exec(event->globalPos());
545     } else if (event->x() >= patternNameWidth + noteAreaWidth + timeAreaWidth + noteAreaWidth
546                && event->x() < patternNameWidth + noteAreaWidth + timeAreaWidth + noteAreaWidth + patternNameWidth){
547         emit channelContextEvent(channel, noteIndex);
548         pPatternMenu->exec(event->globalPos());
549     }
550 }
551