|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649 |
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
-
- # Pixmap Keyboard, a custom Qt widget
- # Copyright (C) 2011-2019 Filipe Coelho <falktx@falktx.com>
- #
- # This program is free software; you can redistribute it and/or
- # modify it under the terms of the GNU General Public License as
- # published by the Free Software Foundation; either version 2 of
- # the License, or any later version.
- #
- # This program is distributed in the hope that it will be useful,
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- # GNU General Public License for more details.
- #
- # For a full copy of the GNU General Public License see the doc/GPL.txt file.
-
- # ------------------------------------------------------------------------------------------------------------
- # Imports (Global)
-
- from PyQt5.QtCore import pyqtSignal, pyqtSlot, qCritical, Qt, QPointF, QRectF, QTimer, QSettings, QSize
- from PyQt5.QtGui import QColor, QFont, QPainter, QPixmap
- from PyQt5.QtWidgets import QMenu, QScrollArea, QWidget
-
- # ------------------------------------------------------------------------------------------------------------
-
- kMidiKey2RectMapHorizontal = [
- QRectF(0, 0, 24, 57), # C
- QRectF(14, 0, 15, 33), # C#
- QRectF(24, 0, 24, 57), # D
- QRectF(42, 0, 15, 33), # D#
- QRectF(48, 0, 24, 57), # E
- QRectF(72, 0, 24, 57), # F
- QRectF(84, 0, 15, 33), # F#
- QRectF(96, 0, 24, 57), # G
- QRectF(112, 0, 15, 33), # G#
- QRectF(120, 0, 24, 57), # A
- QRectF(140, 0, 15, 33), # A#
- QRectF(144, 0, 24, 57), # B
- ]
-
- kMidiKey2RectMapVertical = [
- QRectF(0, 144, 57, 24), # C
- QRectF(0, 139, 33, 15), # C#
- QRectF(0, 120, 57, 24), # D
- QRectF(0, 111, 33, 15), # D#
- QRectF(0, 96, 57, 24), # E
- QRectF(0, 72, 57, 24), # F
- QRectF(0, 69, 33, 15), # F#
- QRectF(0, 48, 57, 24), # G
- QRectF(0, 41, 33, 15), # G#
- QRectF(0, 24, 57, 24), # A
- QRectF(0, 13, 33, 15), # A#
- QRectF(0, 0, 57, 24), # B
- ]
-
- kPcKeys_qwerty = [
- # 1st octave
- str(Qt.Key_Z),
- str(Qt.Key_S),
- str(Qt.Key_X),
- str(Qt.Key_D),
- str(Qt.Key_C),
- str(Qt.Key_V),
- str(Qt.Key_G),
- str(Qt.Key_B),
- str(Qt.Key_H),
- str(Qt.Key_N),
- str(Qt.Key_J),
- str(Qt.Key_M),
- # 2nd octave
- str(Qt.Key_Q),
- str(Qt.Key_2),
- str(Qt.Key_W),
- str(Qt.Key_3),
- str(Qt.Key_E),
- str(Qt.Key_R),
- str(Qt.Key_5),
- str(Qt.Key_T),
- str(Qt.Key_6),
- str(Qt.Key_Y),
- str(Qt.Key_7),
- str(Qt.Key_U),
- # 3rd octave
- str(Qt.Key_I),
- str(Qt.Key_9),
- str(Qt.Key_O),
- str(Qt.Key_0),
- str(Qt.Key_P),
- ]
-
- kPcKeys_qwertz = [
- # 1st octave
- str(Qt.Key_Y),
- str(Qt.Key_S),
- str(Qt.Key_X),
- str(Qt.Key_D),
- str(Qt.Key_C),
- str(Qt.Key_V),
- str(Qt.Key_G),
- str(Qt.Key_B),
- str(Qt.Key_H),
- str(Qt.Key_N),
- str(Qt.Key_J),
- str(Qt.Key_M),
- # 2nd octave
- str(Qt.Key_Q),
- str(Qt.Key_2),
- str(Qt.Key_W),
- str(Qt.Key_3),
- str(Qt.Key_E),
- str(Qt.Key_R),
- str(Qt.Key_5),
- str(Qt.Key_T),
- str(Qt.Key_6),
- str(Qt.Key_Z),
- str(Qt.Key_7),
- str(Qt.Key_U),
- # 3rd octave
- str(Qt.Key_I),
- str(Qt.Key_9),
- str(Qt.Key_O),
- str(Qt.Key_0),
- str(Qt.Key_P),
- ]
-
- kPcKeys_azerty = [
- # 1st octave
- str(Qt.Key_W),
- str(Qt.Key_S),
- str(Qt.Key_X),
- str(Qt.Key_D),
- str(Qt.Key_C),
- str(Qt.Key_V),
- str(Qt.Key_G),
- str(Qt.Key_B),
- str(Qt.Key_H),
- str(Qt.Key_N),
- str(Qt.Key_J),
- str(Qt.Key_Comma),
- # 2nd octave
- str(Qt.Key_A),
- str(Qt.Key_Eacute),
- str(Qt.Key_Z),
- str(Qt.Key_QuoteDbl),
- str(Qt.Key_E),
- str(Qt.Key_R),
- str(Qt.Key_ParenLeft),
- str(Qt.Key_T),
- str(Qt.Key_Minus),
- str(Qt.Key_Y),
- str(Qt.Key_Egrave),
- str(Qt.Key_U),
- # 3rd octave
- str(Qt.Key_I),
- str(Qt.Key_Ccedilla),
- str(Qt.Key_O),
- str(Qt.Key_Agrave),
- str(Qt.Key_P),
- ]
-
- kPcKeysLayouts = {
- 'qwerty': kPcKeys_qwerty,
- 'qwertz': kPcKeys_qwertz,
- 'azerty': kPcKeys_azerty,
- }
-
- kValidColors = ("Blue", "Green", "Orange", "Red")
-
- kBlackNotes = (1, 3, 6, 8, 10)
-
- # ------------------------------------------------------------------------------------------------------------
- # MIDI Keyboard, using a pixmap for painting
-
- class PixmapKeyboard(QWidget):
- # signals
- noteOn = pyqtSignal(int)
- noteOff = pyqtSignal(int)
- notesOn = pyqtSignal()
- notesOff = pyqtSignal()
-
- def __init__(self, parent):
- QWidget.__init__(self, parent)
-
- self.fEnabledKeys = []
- self.fLastMouseNote = -1
- self.fStartOctave = 0
- self.fPcKeybOffset = 2
- self.fInitalizing = True
-
- self.fFont = self.font()
- self.fFont.setFamily("Monospace")
- self.fFont.setPixelSize(12)
- self.fFont.setBold(True)
-
- self.fPixmapNormal = QPixmap(":/bitmaps/kbd_normal.png")
- self.fPixmapDown = QPixmap(":/bitmaps/kbd_down-blue.png")
- self.fHighlightColor = kValidColors[0]
-
- self.fkPcKeyLayout = "qwerty"
- self.fkPcKeys = kPcKeysLayouts["qwerty"]
- self.fKey2RectMap = kMidiKey2RectMapHorizontal
-
- self.fWidth = self.fPixmapNormal.width()
- self.fHeight = self.fPixmapNormal.height()
-
- self.setCursor(Qt.PointingHandCursor)
- self.setStartOctave(0)
- self.setOctaves(6)
-
- self.loadSettings()
-
- self.fInitalizing = False
-
- def saveSettings(self):
- if self.fInitalizing:
- return
-
- settings = QSettings("falkTX", "CarlaKeyboard")
- settings.setValue("PcKeyboardLayout", self.fkPcKeyLayout)
- settings.setValue("PcKeyboardOffset", self.fPcKeybOffset)
- settings.setValue("HighlightColor", self.fHighlightColor)
- del settings
-
- def loadSettings(self):
- settings = QSettings("falkTX", "CarlaKeyboard")
- self.setPcKeyboardLayout(settings.value("PcKeyboardLayout", self.fkPcKeyLayout, type=str))
- self.setPcKeyboardOffset(settings.value("PcKeyboardOffset", self.fPcKeybOffset, type=int))
- self.setColor(settings.value("HighlightColor", self.fHighlightColor, type=str))
- del settings
-
- def allNotesOff(self, sendSignal=True):
- self.fEnabledKeys = []
-
- if sendSignal:
- self.notesOff.emit()
-
- self.update()
-
- def sendNoteOn(self, note, sendSignal=True):
- if 0 <= note <= 127 and note not in self.fEnabledKeys:
- self.fEnabledKeys.append(note)
-
- if sendSignal:
- self.noteOn.emit(note)
-
- self.update()
-
- if len(self.fEnabledKeys) == 1:
- self.notesOn.emit()
-
- def sendNoteOff(self, note, sendSignal=True):
- if 0 <= note <= 127 and note in self.fEnabledKeys:
- self.fEnabledKeys.remove(note)
-
- if sendSignal:
- self.noteOff.emit(note)
-
- self.update()
-
- if len(self.fEnabledKeys) == 0:
- self.notesOff.emit()
-
- def setColor(self, color):
- if color not in kValidColors:
- return
-
- if self.fHighlightColor == color:
- return
-
- self.fHighlightColor = color
- self.fPixmapDown.load(":/bitmaps/kbd_down-{}.png".format(color.lower()))
- self.saveSettings()
-
- def setPcKeyboardLayout(self, layout):
- if layout not in kPcKeysLayouts.keys():
- return
-
- if self.fkPcKeyLayout == layout:
- return
-
- self.fkPcKeyLayout = layout
- self.fkPcKeys = kPcKeysLayouts[layout]
- self.saveSettings()
-
- def setPcKeyboardOffset(self, offset):
- if offset < 0:
- offset = 0
- elif offset > 9:
- offset = 9
-
- if self.fPcKeybOffset == offset:
- return
-
- self.fPcKeybOffset = offset
- self.saveSettings()
-
- def setOctaves(self, octaves):
- if octaves < 1:
- octaves = 1
- elif octaves > 10:
- octaves = 10
-
- self.fOctaves = octaves
-
- self.setMinimumSize(self.fWidth * self.fOctaves, self.fHeight)
- self.setMaximumSize(self.fWidth * self.fOctaves, self.fHeight)
-
- def setStartOctave(self, octave):
- if octave < 0:
- octave = 0
- elif octave > 9:
- octave = 9
-
- if self.fStartOctave == octave:
- return
-
- self.fStartOctave = octave
- self.update()
-
- def handleMousePos(self, pos):
- if pos.x() < 0 or pos.x() > self.fOctaves * self.fWidth:
- return
- octave = int(pos.x() / self.fWidth)
- keyPos = QPointF(pos.x() % self.fWidth, pos.y())
-
- if self.fKey2RectMap[1].contains(keyPos): # C#
- note = 1
- elif self.fKey2RectMap[ 3].contains(keyPos): # D#
- note = 3
- elif self.fKey2RectMap[ 6].contains(keyPos): # F#
- note = 6
- elif self.fKey2RectMap[ 8].contains(keyPos): # G#
- note = 8
- elif self.fKey2RectMap[10].contains(keyPos): # A#
- note = 10
- elif self.fKey2RectMap[ 0].contains(keyPos): # C
- note = 0
- elif self.fKey2RectMap[ 2].contains(keyPos): # D
- note = 2
- elif self.fKey2RectMap[ 4].contains(keyPos): # E
- note = 4
- elif self.fKey2RectMap[ 5].contains(keyPos): # F
- note = 5
- elif self.fKey2RectMap[ 7].contains(keyPos): # G
- note = 7
- elif self.fKey2RectMap[ 9].contains(keyPos): # A
- note = 9
- elif self.fKey2RectMap[11].contains(keyPos): # B
- note = 11
- else:
- note = -1
-
- if note != -1:
- note += (self.fStartOctave + octave) * 12
-
- if self.fLastMouseNote != note:
- self.sendNoteOff(self.fLastMouseNote)
- self.sendNoteOn(note)
-
- elif self.fLastMouseNote != -1:
- self.sendNoteOff(self.fLastMouseNote)
-
- self.fLastMouseNote = note
-
- def showOptions(self, event):
- event.accept()
- menu = QMenu()
-
- menu.addAction("Note: restart carla to apply globally").setEnabled(False)
- menu.addSeparator()
-
- menuColor = QMenu("Highlight color", menu)
- menuLayout = QMenu("PC Keyboard layout", menu)
- actColors = []
- actLayouts = []
-
- for color in kValidColors:
- act = menuColor.addAction(color)
- act.setCheckable(True)
- if self.fHighlightColor == color:
- act.setChecked(True)
- actColors.append(act)
-
- for pcKeyLayout in kPcKeysLayouts.keys():
- act = menuLayout.addAction(pcKeyLayout)
- act.setCheckable(True)
- if self.fkPcKeyLayout == pcKeyLayout:
- act.setChecked(True)
- actLayouts.append(act)
-
- menu.addMenu(menuColor)
- menu.addMenu(menuLayout)
- menu.addSeparator()
-
- actOctaveUp = menu.addAction("PC Keyboard octave up")
- actOctaveDown = menu.addAction("PC Keyboard octave down")
-
- if self.fPcKeybOffset == 0:
- actOctaveDown.setEnabled(False)
-
- actSelected = menu.exec_(event.screenPos().toPoint())
-
- if not actSelected:
- return
-
- if actSelected in actColors:
- return self.setColor(actSelected.text())
-
- if actSelected in actLayouts:
- return self.setPcKeyboardLayout(actSelected.text())
-
- if actSelected == actOctaveUp:
- return self.setPcKeyboardOffset(self.fPcKeybOffset + 1)
-
- if actSelected == actOctaveDown:
- return self.setPcKeyboardOffset(self.fPcKeybOffset - 1)
-
- def minimumSizeHint(self):
- return QSize(self.fWidth, self.fHeight)
-
- def sizeHint(self):
- return QSize(self.fWidth * self.fOctaves, self.fHeight)
-
- def keyPressEvent(self, event):
- if not event.isAutoRepeat():
- try:
- qKey = str(event.key())
- index = self.fkPcKeys.index(qKey)
- except:
- pass
- else:
- self.sendNoteOn(index+(self.fPcKeybOffset*12))
-
- QWidget.keyPressEvent(self, event)
-
- def keyReleaseEvent(self, event):
- if not event.isAutoRepeat():
- try:
- qKey = str(event.key())
- index = self.fkPcKeys.index(qKey)
- except:
- pass
- else:
- self.sendNoteOff(index+(self.fPcKeybOffset*12))
-
- QWidget.keyReleaseEvent(self, event)
-
- def mousePressEvent(self, event):
- if event.button() == Qt.RightButton:
- self.showOptions(event)
- else:
- self.fLastMouseNote = -1
- self.handleMousePos(event.pos())
- self.setFocus()
- QWidget.mousePressEvent(self, event)
-
- def mouseMoveEvent(self, event):
- if event.button() != Qt.RightButton:
- self.handleMousePos(event.pos())
- QWidget.mouseMoveEvent(self, event)
-
- def mouseReleaseEvent(self, event):
- if self.fLastMouseNote != -1:
- self.sendNoteOff(self.fLastMouseNote)
- self.fLastMouseNote = -1
- QWidget.mouseReleaseEvent(self, event)
-
- def paintEvent(self, event):
- painter = QPainter(self)
- event.accept()
-
- # -------------------------------------------------------------
- # Paint clean keys (as background)
-
- for octave in range(self.fOctaves):
- target = QRectF(self.fWidth * octave, 0, self.fWidth, self.fHeight)
- source = QRectF(0, 0, self.fWidth, self.fHeight)
- painter.drawPixmap(target, self.fPixmapNormal, source)
-
- if not self.isEnabled():
- painter.setBrush(QColor(0, 0, 0, 150))
- painter.setPen(QColor(0, 0, 0, 150))
- painter.drawRect(0, 0, self.width(), self.height())
- return
-
- # -------------------------------------------------------------
- # Paint (white) pressed keys
-
- paintedWhite = False
-
- for note in self.fEnabledKeys:
- pos = self._getRectFromMidiNote(note)
-
- if self._isNoteBlack(note):
- continue
-
- if note < 12:
- octave = 0
- elif note < 24:
- octave = 1
- elif note < 36:
- octave = 2
- elif note < 48:
- octave = 3
- elif note < 60:
- octave = 4
- elif note < 72:
- octave = 5
- elif note < 84:
- octave = 6
- elif note < 96:
- octave = 7
- elif note < 108:
- octave = 8
- elif note < 120:
- octave = 9
- elif note < 132:
- octave = 10
- else:
- # cannot paint this note
- continue
-
- octave -= self.fStartOctave
-
- target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
- source = QRectF(pos.x(), 0, pos.width(), pos.height())
-
- paintedWhite = True
- painter.drawPixmap(target, self.fPixmapDown, source)
-
- # -------------------------------------------------------------
- # Clear white keys border
-
- if paintedWhite:
- for octave in range(self.fOctaves):
- for note in kBlackNotes:
- pos = self._getRectFromMidiNote(note)
-
- target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
- source = QRectF(pos.x(), 0, pos.width(), pos.height())
-
- painter.drawPixmap(target, self.fPixmapNormal, source)
-
- # -------------------------------------------------------------
- # Paint (black) pressed keys
-
- for note in self.fEnabledKeys:
- pos = self._getRectFromMidiNote(note)
-
- if not self._isNoteBlack(note):
- continue
-
- if note < 12:
- octave = 0
- elif note < 24:
- octave = 1
- elif note < 36:
- octave = 2
- elif note < 48:
- octave = 3
- elif note < 60:
- octave = 4
- elif note < 72:
- octave = 5
- elif note < 84:
- octave = 6
- elif note < 96:
- octave = 7
- elif note < 108:
- octave = 8
- elif note < 120:
- octave = 9
- elif note < 132:
- octave = 10
- else:
- # cannot paint this note
- continue
-
- octave -= self.fStartOctave
-
- target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height())
- source = QRectF(pos.x(), 0, pos.width(), pos.height())
-
- painter.drawPixmap(target, self.fPixmapDown, source)
-
- # Paint C-number note info
- painter.setFont(self.fFont)
- painter.setPen(Qt.black)
-
- for i in range(self.fOctaves):
- octave = self.fStartOctave + i - 1
- painter.drawText(i * 168 + (4 if octave == -1 else 3),
- 35, 20, 20,
- Qt.AlignCenter,
- "C{}".format(octave))
-
- def _isNoteBlack(self, note):
- baseNote = note % 12
- return bool(baseNote in kBlackNotes)
-
- def _getRectFromMidiNote(self, note):
- baseNote = note % 12
- return self.fKey2RectMap[baseNote]
-
- # ------------------------------------------------------------------------------------------------------------
- # Horizontal scroll area for keyboard
-
- class PixmapKeyboardHArea(QScrollArea):
- def __init__(self, parent):
- QScrollArea.__init__(self, parent)
-
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn)
- self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-
- self.keyboard = PixmapKeyboard(self)
- self.keyboard.setOctaves(10)
- self.setWidget(self.keyboard)
-
- self.setEnabled(False)
- self.setFixedHeight(self.keyboard.height() + self.horizontalScrollBar().height()/2 + 2)
-
- QTimer.singleShot(0, self.slot_initScrollbarValue)
-
- # FIXME use change event
- def setEnabled(self, yesNo):
- self.keyboard.setEnabled(yesNo)
- QScrollArea.setEnabled(self, yesNo)
-
- @pyqtSlot()
- def slot_initScrollbarValue(self):
- self.horizontalScrollBar().setValue(self.horizontalScrollBar().maximum()/2)
-
- # ------------------------------------------------------------------------------------------------------------
- # Main Testing
-
- if __name__ == '__main__':
- import sys
- from PyQt5.QtWidgets import QApplication
- import resources_rc
-
- app = QApplication(sys.argv)
-
- gui = PixmapKeyboard(None)
- gui.setEnabled(True)
- gui.show()
-
- sys.exit(app.exec_())
|