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, ¬eIndex)) {
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, ¬eIndex)) {
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