2 # SPDX-FileCopyrightText: 2011-2024 Filipe Coelho <falktx@falktx.com>
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 # ---------------------------------------------------------------------------------------------------------------------
8 from qt_compat
import qt_config
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
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 # ---------------------------------------------------------------------------------------------------------------------
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
144 str(Qt
.Key_QuoteDbl
),
147 str(Qt
.Key_ParenLeft
),
155 str(Qt
.Key_Ccedilla
),
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
):
175 return bool(baseNote
in kBlackNotes
)
177 # ------------------------------------------------------------------------------------------------------------
178 # MIDI Keyboard, using a pixmap for painting
180 class PixmapKeyboard(QWidget
):
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)
218 self
.fInitalizing
= False
220 def saveSettings(self
):
221 if self
.fInitalizing
:
224 settings
= QSafeSettings("falkTX", "CarlaKeyboard")
225 settings
.setValue("PcKeyboardLayout", self
.fkPcKeyLayout
)
226 settings
.setValue("PcKeyboardOffset", self
.fPcKeybOffset
)
227 settings
.setValue("HighlightColor", self
.fHighlightColor
)
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))
237 def allNotesOff(self
, sendSignal
=True):
238 self
.fEnabledKeys
= []
245 def sendNoteOn(self
, note
, sendSignal
=True):
246 if 0 <= note
<= 127 and note
not in self
.fEnabledKeys
:
247 self
.fEnabledKeys
.append(note
)
250 self
.noteOn
.emit(note
)
254 if len(self
.fEnabledKeys
) == 1:
257 def sendNoteOff(self
, note
, sendSignal
=True):
258 if 0 <= note
<= 127 and note
in self
.fEnabledKeys
:
259 self
.fEnabledKeys
.remove(note
)
262 self
.noteOff
.emit(note
)
266 if len(self
.fEnabledKeys
) == 0:
269 def setColor(self
, color
):
270 if color
not in kValidColors
:
273 if self
.fHighlightColor
== color
:
276 self
.fHighlightColor
= color
277 self
.fPixmapDown
.load(":/bitmaps/kbd_down-{}.png".format(color
.lower()))
280 def setPcKeyboardLayout(self
, layout
):
281 if layout
not in kPcKeysLayouts
.keys():
284 if self
.fkPcKeyLayout
== layout
:
287 self
.fkPcKeyLayout
= layout
288 self
.fkPcKeys
= kPcKeysLayouts
[layout
]
291 def setPcKeyboardOffset(self
, offset
):
297 if self
.fPcKeybOffset
== offset
:
300 self
.fPcKeybOffset
= offset
303 def setOctaves(self
, octaves
):
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
):
320 if self
.fStartOctave
== octave
:
323 self
.fStartOctave
= octave
326 def handleMousePos(self
, pos
):
327 if pos
.x() < 0 or pos
.x() > self
.fOctaves
* self
.fWidth
:
329 octave
= int(pos
.x() / self
.fWidth
)
330 keyPos
= QPointF(pos
.x() % self
.fWidth
, pos
.y())
332 if self
.fKey2RectMap
[1].contains(keyPos
): # C#
334 elif self
.fKey2RectMap
[ 3].contains(keyPos
): # D#
336 elif self
.fKey2RectMap
[ 6].contains(keyPos
): # F#
338 elif self
.fKey2RectMap
[ 8].contains(keyPos
): # G#
340 elif self
.fKey2RectMap
[10].contains(keyPos
): # A#
342 elif self
.fKey2RectMap
[ 0].contains(keyPos
): # C
344 elif self
.fKey2RectMap
[ 2].contains(keyPos
): # D
346 elif self
.fKey2RectMap
[ 4].contains(keyPos
): # E
348 elif self
.fKey2RectMap
[ 5].contains(keyPos
): # F
350 elif self
.fKey2RectMap
[ 7].contains(keyPos
): # G
352 elif self
.fKey2RectMap
[ 9].contains(keyPos
): # A
354 elif self
.fKey2RectMap
[11].contains(keyPos
): # B
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
):
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
)
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
:
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
:
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())
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
)
432 return QSize(self
.fWidth
* self
.fOctaves
, self
.fHeight
)
434 def keyPressEvent(self
, event
):
435 if not event
.isAutoRepeat():
437 qKey
= str(event
.key())
438 index
= self
.fkPcKeys
.index(qKey
)
442 self
.sendNoteOn(index
+(self
.fPcKeybOffset
*12))
444 QWidget
.keyPressEvent(self
, event
)
446 def keyReleaseEvent(self
, event
):
447 if not event
.isAutoRepeat():
449 qKey
= str(event
.key())
450 index
= self
.fkPcKeys
.index(qKey
)
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
)
462 self
.fLastMouseNote
= -1
463 self
.handleMousePos(event
.pos())
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
)
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())
496 # -------------------------------------------------------------
497 # Paint (white) pressed keys
501 for note
in self
.fEnabledKeys
:
502 pos
= self
._getRectFromMidiNote
(note
)
504 if _isNoteBlack(note
):
530 # cannot paint this note
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())
539 painter
.drawPixmap(target
, self
.fPixmapDown
, source
)
541 # -------------------------------------------------------------
542 # Clear white keys border
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
):
586 # cannot paint this note
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),
605 "C{}".format(octave
))
607 def _getRectFromMidiNote(self
, note
):
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
)
636 def slot_initScrollbarValue(self
):
637 self
.horizontalScrollBar().setValue(int(self
.horizontalScrollBar().maximum()/2))
639 # ---------------------------------------------------------------------------------------------------------------------