Cleanup
[carla.git] / source / frontend / widgets / pixmapkeyboard.py
bloba81e687f847dd60177d0760a5d25e622907c5503
1 #!/usr/bin/env python3
2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ---------------------------------------------------------------------------------------------------------------------
6 # Imports (Global)
8 from qt_compat import qt_config
10 if qt_config == 5:
11 from PyQt5.QtCore import pyqtSignal, pyqtSlot, Qt, QPointF, QRectF, QTimer, QSize
12 from PyQt5.QtGui import QColor, QPainter, QPixmap
13 from PyQt5.QtWidgets import QActionGroup, QMenu, QScrollArea, QWidget
14 elif qt_config == 6:
15 from PyQt6.QtCore import pyqtSignal, pyqtSlot, Qt, QPointF, QRectF, QTimer, QSize
16 from PyQt6.QtGui import QActionGroup, QColor, QPainter, QPixmap
17 from PyQt6.QtWidgets import QMenu, QScrollArea, QWidget
19 # ---------------------------------------------------------------------------------------------------------------------
20 # Imports (Carla)
22 from utils import QSafeSettings
24 # ---------------------------------------------------------------------------------------------------------------------
26 kMidiKey2RectMapHorizontal = [
27 QRectF(0, 0, 24, 57), # C
28 QRectF(14, 0, 15, 33), # C#
29 QRectF(24, 0, 24, 57), # D
30 QRectF(42, 0, 15, 33), # D#
31 QRectF(48, 0, 24, 57), # E
32 QRectF(72, 0, 24, 57), # F
33 QRectF(84, 0, 15, 33), # F#
34 QRectF(96, 0, 24, 57), # G
35 QRectF(112, 0, 15, 33), # G#
36 QRectF(120, 0, 24, 57), # A
37 QRectF(140, 0, 15, 33), # A#
38 QRectF(144, 0, 24, 57), # B
41 kMidiKey2RectMapVertical = [
42 QRectF(0, 144, 57, 24), # C
43 QRectF(0, 139, 33, 15), # C#
44 QRectF(0, 120, 57, 24), # D
45 QRectF(0, 111, 33, 15), # D#
46 QRectF(0, 96, 57, 24), # E
47 QRectF(0, 72, 57, 24), # F
48 QRectF(0, 69, 33, 15), # F#
49 QRectF(0, 48, 57, 24), # G
50 QRectF(0, 41, 33, 15), # G#
51 QRectF(0, 24, 57, 24), # A
52 QRectF(0, 13, 33, 15), # A#
53 QRectF(0, 0, 57, 24), # B
56 kPcKeys_qwerty = [
57 # 1st octave
58 str(Qt.Key_Z),
59 str(Qt.Key_S),
60 str(Qt.Key_X),
61 str(Qt.Key_D),
62 str(Qt.Key_C),
63 str(Qt.Key_V),
64 str(Qt.Key_G),
65 str(Qt.Key_B),
66 str(Qt.Key_H),
67 str(Qt.Key_N),
68 str(Qt.Key_J),
69 str(Qt.Key_M),
70 # 2nd octave
71 str(Qt.Key_Q),
72 str(Qt.Key_2),
73 str(Qt.Key_W),
74 str(Qt.Key_3),
75 str(Qt.Key_E),
76 str(Qt.Key_R),
77 str(Qt.Key_5),
78 str(Qt.Key_T),
79 str(Qt.Key_6),
80 str(Qt.Key_Y),
81 str(Qt.Key_7),
82 str(Qt.Key_U),
83 # 3rd octave
84 str(Qt.Key_I),
85 str(Qt.Key_9),
86 str(Qt.Key_O),
87 str(Qt.Key_0),
88 str(Qt.Key_P),
91 kPcKeys_qwertz = [
92 # 1st octave
93 str(Qt.Key_Y),
94 str(Qt.Key_S),
95 str(Qt.Key_X),
96 str(Qt.Key_D),
97 str(Qt.Key_C),
98 str(Qt.Key_V),
99 str(Qt.Key_G),
100 str(Qt.Key_B),
101 str(Qt.Key_H),
102 str(Qt.Key_N),
103 str(Qt.Key_J),
104 str(Qt.Key_M),
105 # 2nd octave
106 str(Qt.Key_Q),
107 str(Qt.Key_2),
108 str(Qt.Key_W),
109 str(Qt.Key_3),
110 str(Qt.Key_E),
111 str(Qt.Key_R),
112 str(Qt.Key_5),
113 str(Qt.Key_T),
114 str(Qt.Key_6),
115 str(Qt.Key_Z),
116 str(Qt.Key_7),
117 str(Qt.Key_U),
118 # 3rd octave
119 str(Qt.Key_I),
120 str(Qt.Key_9),
121 str(Qt.Key_O),
122 str(Qt.Key_0),
123 str(Qt.Key_P),
126 kPcKeys_azerty = [
127 # 1st octave
128 str(Qt.Key_W),
129 str(Qt.Key_S),
130 str(Qt.Key_X),
131 str(Qt.Key_D),
132 str(Qt.Key_C),
133 str(Qt.Key_V),
134 str(Qt.Key_G),
135 str(Qt.Key_B),
136 str(Qt.Key_H),
137 str(Qt.Key_N),
138 str(Qt.Key_J),
139 str(Qt.Key_Comma),
140 # 2nd octave
141 str(Qt.Key_A),
142 str(Qt.Key_Eacute),
143 str(Qt.Key_Z),
144 str(Qt.Key_QuoteDbl),
145 str(Qt.Key_E),
146 str(Qt.Key_R),
147 str(Qt.Key_ParenLeft),
148 str(Qt.Key_T),
149 str(Qt.Key_Minus),
150 str(Qt.Key_Y),
151 str(Qt.Key_Egrave),
152 str(Qt.Key_U),
153 # 3rd octave
154 str(Qt.Key_I),
155 str(Qt.Key_Ccedilla),
156 str(Qt.Key_O),
157 str(Qt.Key_Agrave),
158 str(Qt.Key_P),
161 kPcKeysLayouts = {
162 'qwerty': kPcKeys_qwerty,
163 'qwertz': kPcKeys_qwertz,
164 'azerty': kPcKeys_azerty,
167 kValidColors = ("Blue", "Green", "Orange", "Red")
169 kBlackNotes = (1, 3, 6, 8, 10)
171 # ------------------------------------------------------------------------------------------------------------
173 def _isNoteBlack(note):
174 baseNote = note % 12
175 return bool(baseNote in kBlackNotes)
177 # ------------------------------------------------------------------------------------------------------------
178 # MIDI Keyboard, using a pixmap for painting
180 class PixmapKeyboard(QWidget):
181 # signals
182 noteOn = pyqtSignal(int)
183 noteOff = pyqtSignal(int)
184 notesOn = pyqtSignal()
185 notesOff = pyqtSignal()
187 def __init__(self, parent):
188 QWidget.__init__(self, parent)
190 self.fEnabledKeys = []
191 self.fLastMouseNote = -1
192 self.fStartOctave = 0
193 self.fPcKeybOffset = 2
194 self.fInitalizing = True
196 self.fFont = self.font()
197 self.fFont.setFamily("Monospace")
198 self.fFont.setPixelSize(12)
199 self.fFont.setBold(True)
201 self.fPixmapNormal = QPixmap(":/bitmaps/kbd_normal.png")
202 self.fPixmapDown = QPixmap(":/bitmaps/kbd_down-blue.png")
203 self.fHighlightColor = kValidColors[0]
205 self.fkPcKeyLayout = "qwerty"
206 self.fkPcKeys = kPcKeysLayouts["qwerty"]
207 self.fKey2RectMap = kMidiKey2RectMapHorizontal
209 self.fWidth = self.fPixmapNormal.width()
210 self.fHeight = self.fPixmapNormal.height()
212 self.setCursor(Qt.PointingHandCursor)
213 self.setStartOctave(0)
214 self.setOctaves(6)
216 self.loadSettings()
218 self.fInitalizing = False
220 def saveSettings(self):
221 if self.fInitalizing:
222 return
224 settings = QSafeSettings("falkTX", "CarlaKeyboard")
225 settings.setValue("PcKeyboardLayout", self.fkPcKeyLayout)
226 settings.setValue("PcKeyboardOffset", self.fPcKeybOffset)
227 settings.setValue("HighlightColor", self.fHighlightColor)
228 del settings
230 def loadSettings(self):
231 settings = QSafeSettings("falkTX", "CarlaKeyboard")
232 self.setPcKeyboardLayout(settings.value("PcKeyboardLayout", self.fkPcKeyLayout, str))
233 self.setPcKeyboardOffset(settings.value("PcKeyboardOffset", self.fPcKeybOffset, int))
234 self.setColor(settings.value("HighlightColor", self.fHighlightColor, str))
235 del settings
237 def allNotesOff(self, sendSignal=True):
238 self.fEnabledKeys = []
240 if sendSignal:
241 self.notesOff.emit()
243 self.update()
245 def sendNoteOn(self, note, sendSignal=True):
246 if 0 <= note <= 127 and note not in self.fEnabledKeys:
247 self.fEnabledKeys.append(note)
249 if sendSignal:
250 self.noteOn.emit(note)
252 self.update()
254 if len(self.fEnabledKeys) == 1:
255 self.notesOn.emit()
257 def sendNoteOff(self, note, sendSignal=True):
258 if 0 <= note <= 127 and note in self.fEnabledKeys:
259 self.fEnabledKeys.remove(note)
261 if sendSignal:
262 self.noteOff.emit(note)
264 self.update()
266 if len(self.fEnabledKeys) == 0:
267 self.notesOff.emit()
269 def setColor(self, color):
270 if color not in kValidColors:
271 return
273 if self.fHighlightColor == color:
274 return
276 self.fHighlightColor = color
277 self.fPixmapDown.load(":/bitmaps/kbd_down-{}.png".format(color.lower()))
278 self.saveSettings()
280 def setPcKeyboardLayout(self, layout):
281 if layout not in kPcKeysLayouts.keys():
282 return
284 if self.fkPcKeyLayout == layout:
285 return
287 self.fkPcKeyLayout = layout
288 self.fkPcKeys = kPcKeysLayouts[layout]
289 self.saveSettings()
291 def setPcKeyboardOffset(self, offset):
292 if offset < 0:
293 offset = 0
294 elif offset > 9:
295 offset = 9
297 if self.fPcKeybOffset == offset:
298 return
300 self.fPcKeybOffset = offset
301 self.saveSettings()
303 def setOctaves(self, octaves):
304 if octaves < 1:
305 octaves = 1
306 elif octaves > 10:
307 octaves = 10
309 self.fOctaves = octaves
311 self.setMinimumSize(self.fWidth * self.fOctaves, self.fHeight)
312 self.setMaximumSize(self.fWidth * self.fOctaves, self.fHeight)
314 def setStartOctave(self, octave):
315 if octave < 0:
316 octave = 0
317 elif octave > 9:
318 octave = 9
320 if self.fStartOctave == octave:
321 return
323 self.fStartOctave = octave
324 self.update()
326 def handleMousePos(self, pos):
327 if pos.x() < 0 or pos.x() > self.fOctaves * self.fWidth:
328 return
329 octave = int(pos.x() / self.fWidth)
330 keyPos = QPointF(pos.x() % self.fWidth, pos.y())
332 if self.fKey2RectMap[1].contains(keyPos): # C#
333 note = 1
334 elif self.fKey2RectMap[ 3].contains(keyPos): # D#
335 note = 3
336 elif self.fKey2RectMap[ 6].contains(keyPos): # F#
337 note = 6
338 elif self.fKey2RectMap[ 8].contains(keyPos): # G#
339 note = 8
340 elif self.fKey2RectMap[10].contains(keyPos): # A#
341 note = 10
342 elif self.fKey2RectMap[ 0].contains(keyPos): # C
343 note = 0
344 elif self.fKey2RectMap[ 2].contains(keyPos): # D
345 note = 2
346 elif self.fKey2RectMap[ 4].contains(keyPos): # E
347 note = 4
348 elif self.fKey2RectMap[ 5].contains(keyPos): # F
349 note = 5
350 elif self.fKey2RectMap[ 7].contains(keyPos): # G
351 note = 7
352 elif self.fKey2RectMap[ 9].contains(keyPos): # A
353 note = 9
354 elif self.fKey2RectMap[11].contains(keyPos): # B
355 note = 11
356 else:
357 note = -1
359 if note != -1:
360 note += (self.fStartOctave + octave) * 12
362 if self.fLastMouseNote != note:
363 self.sendNoteOff(self.fLastMouseNote)
364 self.sendNoteOn(note)
366 elif self.fLastMouseNote != -1:
367 self.sendNoteOff(self.fLastMouseNote)
369 self.fLastMouseNote = note
371 def showOptions(self, event):
372 event.accept()
373 menu = QMenu()
375 menu.addAction(self.tr("Note: restart carla to apply globally")).setEnabled(False)
376 menu.addAction(self.tr("Color")).setSeparator(True)
378 groupColor = QActionGroup(menu)
379 groupLayout = QActionGroup(menu)
380 actColors = []
381 actLayouts = []
383 menu.addAction(self.tr("Highlight color")).setSeparator(True)
385 for color in kValidColors:
386 act = menu.addAction(color)
387 act.setActionGroup(groupColor)
388 act.setCheckable(True)
389 if self.fHighlightColor == color:
390 act.setChecked(True)
391 actColors.append(act)
393 menu.addAction(self.tr("PC Keyboard layout")).setSeparator(True)
395 for pcKeyLayout in kPcKeysLayouts.keys():
396 act = menu.addAction(pcKeyLayout)
397 act.setActionGroup(groupLayout)
398 act.setCheckable(True)
399 if self.fkPcKeyLayout == pcKeyLayout:
400 act.setChecked(True)
401 actLayouts.append(act)
403 menu.addAction(self.tr("PC Keyboard base octave (%i)" % self.fPcKeybOffset)).setSeparator(True)
405 actOctaveUp = menu.addAction(self.tr("Octave up"))
406 actOctaveDown = menu.addAction(self.tr("Octave down"))
408 if self.fPcKeybOffset == 0:
409 actOctaveDown.setEnabled(False)
411 actSelected = menu.exec_(event.screenPos().toPoint())
413 if not actSelected:
414 return
416 if actSelected in actColors:
417 return self.setColor(actSelected.text())
419 if actSelected in actLayouts:
420 return self.setPcKeyboardLayout(actSelected.text())
422 if actSelected == actOctaveUp:
423 return self.setPcKeyboardOffset(self.fPcKeybOffset + 1)
425 if actSelected == actOctaveDown:
426 return self.setPcKeyboardOffset(self.fPcKeybOffset - 1)
428 def minimumSizeHint(self):
429 return QSize(self.fWidth, self.fHeight)
431 def sizeHint(self):
432 return QSize(self.fWidth * self.fOctaves, self.fHeight)
434 def keyPressEvent(self, event):
435 if not event.isAutoRepeat():
436 try:
437 qKey = str(event.key())
438 index = self.fkPcKeys.index(qKey)
439 except:
440 pass
441 else:
442 self.sendNoteOn(index+(self.fPcKeybOffset*12))
444 QWidget.keyPressEvent(self, event)
446 def keyReleaseEvent(self, event):
447 if not event.isAutoRepeat():
448 try:
449 qKey = str(event.key())
450 index = self.fkPcKeys.index(qKey)
451 except:
452 pass
453 else:
454 self.sendNoteOff(index+(self.fPcKeybOffset*12))
456 QWidget.keyReleaseEvent(self, event)
458 def mousePressEvent(self, event):
459 if event.button() == Qt.RightButton:
460 self.showOptions(event)
461 else:
462 self.fLastMouseNote = -1
463 self.handleMousePos(event.pos())
464 self.setFocus()
465 QWidget.mousePressEvent(self, event)
467 def mouseMoveEvent(self, event):
468 if event.button() != Qt.RightButton:
469 self.handleMousePos(event.pos())
470 QWidget.mouseMoveEvent(self, event)
472 def mouseReleaseEvent(self, event):
473 if self.fLastMouseNote != -1:
474 self.sendNoteOff(self.fLastMouseNote)
475 self.fLastMouseNote = -1
476 QWidget.mouseReleaseEvent(self, event)
478 def paintEvent(self, event):
479 painter = QPainter(self)
480 event.accept()
482 # -------------------------------------------------------------
483 # Paint clean keys (as background)
485 for octave in range(self.fOctaves):
486 target = QRectF(self.fWidth * octave, 0, self.fWidth, self.fHeight)
487 source = QRectF(0, 0, self.fWidth, self.fHeight)
488 painter.drawPixmap(target, self.fPixmapNormal, source)
490 if not self.isEnabled():
491 painter.setBrush(QColor(0, 0, 0, 150))
492 painter.setPen(QColor(0, 0, 0, 150))
493 painter.drawRect(0, 0, self.width(), self.height())
494 return
496 # -------------------------------------------------------------
497 # Paint (white) pressed keys
499 paintedWhite = False
501 for note in self.fEnabledKeys:
502 pos = self._getRectFromMidiNote(note)
504 if _isNoteBlack(note):
505 continue
507 if note < 12:
508 octave = 0
509 elif note < 24:
510 octave = 1
511 elif note < 36:
512 octave = 2
513 elif note < 48:
514 octave = 3
515 elif note < 60:
516 octave = 4
517 elif note < 72:
518 octave = 5
519 elif note < 84:
520 octave = 6
521 elif note < 96:
522 octave = 7
523 elif note < 108:
524 octave = 8
525 elif note < 120:
526 octave = 9
527 elif note < 132:
528 octave = 10
529 else:
530 # cannot paint this note
531 continue
533 octave -= self.fStartOctave
535 target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
536 source = QRectF(pos.x(), 0, pos.width(), pos.height())
538 paintedWhite = True
539 painter.drawPixmap(target, self.fPixmapDown, source)
541 # -------------------------------------------------------------
542 # Clear white keys border
544 if paintedWhite:
545 for octave in range(self.fOctaves):
546 for note in kBlackNotes:
547 pos = self._getRectFromMidiNote(note)
549 target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
550 source = QRectF(pos.x(), 0, pos.width(), pos.height())
552 painter.drawPixmap(target, self.fPixmapNormal, source)
554 # -------------------------------------------------------------
555 # Paint (black) pressed keys
557 for note in self.fEnabledKeys:
558 pos = self._getRectFromMidiNote(note)
560 if not _isNoteBlack(note):
561 continue
563 if note < 12:
564 octave = 0
565 elif note < 24:
566 octave = 1
567 elif note < 36:
568 octave = 2
569 elif note < 48:
570 octave = 3
571 elif note < 60:
572 octave = 4
573 elif note < 72:
574 octave = 5
575 elif note < 84:
576 octave = 6
577 elif note < 96:
578 octave = 7
579 elif note < 108:
580 octave = 8
581 elif note < 120:
582 octave = 9
583 elif note < 132:
584 octave = 10
585 else:
586 # cannot paint this note
587 continue
589 octave -= self.fStartOctave
591 target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
592 source = QRectF(pos.x(), 0, pos.width(), pos.height())
594 painter.drawPixmap(target, self.fPixmapDown, source)
596 # Paint C-number note info
597 painter.setFont(self.fFont)
598 painter.setPen(Qt.black)
600 for i in range(self.fOctaves):
601 octave = self.fStartOctave + i - 1
602 painter.drawText(i * 168 + (4 if octave == -1 else 3),
603 35, 20, 20,
604 Qt.AlignCenter,
605 "C{}".format(octave))
607 def _getRectFromMidiNote(self, note):
608 baseNote = note % 12
609 return self.fKey2RectMap[baseNote]
611 # ---------------------------------------------------------------------------------------------------------------------
612 # Horizontal scroll area for keyboard
614 class PixmapKeyboardHArea(QScrollArea):
615 def __init__(self, parent):
616 QScrollArea.__init__(self, parent)
618 self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
619 self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
621 self.keyboard = PixmapKeyboard(self)
622 self.keyboard.setOctaves(10)
623 self.setWidget(self.keyboard)
625 self.setEnabled(False)
626 self.setFixedHeight(int(self.keyboard.height() + self.horizontalScrollBar().height()/2 + 2))
628 QTimer.singleShot(0, self.slot_initScrollbarValue)
630 # FIXME use change event
631 def setEnabled(self, yesNo):
632 self.keyboard.setEnabled(yesNo)
633 QScrollArea.setEnabled(self, yesNo)
635 @pyqtSlot()
636 def slot_initScrollbarValue(self):
637 self.horizontalScrollBar().setValue(int(self.horizontalScrollBar().maximum()/2))
639 # ---------------------------------------------------------------------------------------------------------------------