1#!/usr/local/bin/python3.8 2# -*- coding: utf-8 -*- 3 4# Pixmap Keyboard, a custom Qt widget 5# Copyright (C) 2011-2020 Filipe Coelho <falktx@falktx.com> 6# 7# This program is free software; you can redistribute it and/or 8# modify it under the terms of the GNU General Public License as 9# published by the Free Software Foundation; either version 2 of 10# the License, or any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# For a full copy of the GNU General Public License see the doc/GPL.txt file. 18 19# ------------------------------------------------------------------------------------------------------------ 20# Imports (Global) 21 22from PyQt5.QtCore import pyqtSignal, pyqtSlot, qCritical, Qt, QPointF, QRectF, QTimer, QSize 23from PyQt5.QtGui import QColor, QFont, QPainter, QPixmap 24from PyQt5.QtWidgets import QActionGroup, QMenu, QScrollArea, QWidget 25 26# ------------------------------------------------------------------------------------------------------------ 27# Imports (Custom) 28 29from carla_shared import QSafeSettings 30 31# ------------------------------------------------------------------------------------------------------------ 32 33kMidiKey2RectMapHorizontal = [ 34 QRectF(0, 0, 24, 57), # C 35 QRectF(14, 0, 15, 33), # C# 36 QRectF(24, 0, 24, 57), # D 37 QRectF(42, 0, 15, 33), # D# 38 QRectF(48, 0, 24, 57), # E 39 QRectF(72, 0, 24, 57), # F 40 QRectF(84, 0, 15, 33), # F# 41 QRectF(96, 0, 24, 57), # G 42 QRectF(112, 0, 15, 33), # G# 43 QRectF(120, 0, 24, 57), # A 44 QRectF(140, 0, 15, 33), # A# 45 QRectF(144, 0, 24, 57), # B 46] 47 48kMidiKey2RectMapVertical = [ 49 QRectF(0, 144, 57, 24), # C 50 QRectF(0, 139, 33, 15), # C# 51 QRectF(0, 120, 57, 24), # D 52 QRectF(0, 111, 33, 15), # D# 53 QRectF(0, 96, 57, 24), # E 54 QRectF(0, 72, 57, 24), # F 55 QRectF(0, 69, 33, 15), # F# 56 QRectF(0, 48, 57, 24), # G 57 QRectF(0, 41, 33, 15), # G# 58 QRectF(0, 24, 57, 24), # A 59 QRectF(0, 13, 33, 15), # A# 60 QRectF(0, 0, 57, 24), # B 61] 62 63kPcKeys_qwerty = [ 64 # 1st octave 65 str(Qt.Key_Z), 66 str(Qt.Key_S), 67 str(Qt.Key_X), 68 str(Qt.Key_D), 69 str(Qt.Key_C), 70 str(Qt.Key_V), 71 str(Qt.Key_G), 72 str(Qt.Key_B), 73 str(Qt.Key_H), 74 str(Qt.Key_N), 75 str(Qt.Key_J), 76 str(Qt.Key_M), 77 # 2nd octave 78 str(Qt.Key_Q), 79 str(Qt.Key_2), 80 str(Qt.Key_W), 81 str(Qt.Key_3), 82 str(Qt.Key_E), 83 str(Qt.Key_R), 84 str(Qt.Key_5), 85 str(Qt.Key_T), 86 str(Qt.Key_6), 87 str(Qt.Key_Y), 88 str(Qt.Key_7), 89 str(Qt.Key_U), 90 # 3rd octave 91 str(Qt.Key_I), 92 str(Qt.Key_9), 93 str(Qt.Key_O), 94 str(Qt.Key_0), 95 str(Qt.Key_P), 96] 97 98kPcKeys_qwertz = [ 99 # 1st octave 100 str(Qt.Key_Y), 101 str(Qt.Key_S), 102 str(Qt.Key_X), 103 str(Qt.Key_D), 104 str(Qt.Key_C), 105 str(Qt.Key_V), 106 str(Qt.Key_G), 107 str(Qt.Key_B), 108 str(Qt.Key_H), 109 str(Qt.Key_N), 110 str(Qt.Key_J), 111 str(Qt.Key_M), 112 # 2nd octave 113 str(Qt.Key_Q), 114 str(Qt.Key_2), 115 str(Qt.Key_W), 116 str(Qt.Key_3), 117 str(Qt.Key_E), 118 str(Qt.Key_R), 119 str(Qt.Key_5), 120 str(Qt.Key_T), 121 str(Qt.Key_6), 122 str(Qt.Key_Z), 123 str(Qt.Key_7), 124 str(Qt.Key_U), 125 # 3rd octave 126 str(Qt.Key_I), 127 str(Qt.Key_9), 128 str(Qt.Key_O), 129 str(Qt.Key_0), 130 str(Qt.Key_P), 131] 132 133kPcKeys_azerty = [ 134 # 1st octave 135 str(Qt.Key_W), 136 str(Qt.Key_S), 137 str(Qt.Key_X), 138 str(Qt.Key_D), 139 str(Qt.Key_C), 140 str(Qt.Key_V), 141 str(Qt.Key_G), 142 str(Qt.Key_B), 143 str(Qt.Key_H), 144 str(Qt.Key_N), 145 str(Qt.Key_J), 146 str(Qt.Key_Comma), 147 # 2nd octave 148 str(Qt.Key_A), 149 str(Qt.Key_Eacute), 150 str(Qt.Key_Z), 151 str(Qt.Key_QuoteDbl), 152 str(Qt.Key_E), 153 str(Qt.Key_R), 154 str(Qt.Key_ParenLeft), 155 str(Qt.Key_T), 156 str(Qt.Key_Minus), 157 str(Qt.Key_Y), 158 str(Qt.Key_Egrave), 159 str(Qt.Key_U), 160 # 3rd octave 161 str(Qt.Key_I), 162 str(Qt.Key_Ccedilla), 163 str(Qt.Key_O), 164 str(Qt.Key_Agrave), 165 str(Qt.Key_P), 166] 167 168kPcKeysLayouts = { 169 'qwerty': kPcKeys_qwerty, 170 'qwertz': kPcKeys_qwertz, 171 'azerty': kPcKeys_azerty, 172} 173 174kValidColors = ("Blue", "Green", "Orange", "Red") 175 176kBlackNotes = (1, 3, 6, 8, 10) 177 178# ------------------------------------------------------------------------------------------------------------ 179# MIDI Keyboard, using a pixmap for painting 180 181class PixmapKeyboard(QWidget): 182 # signals 183 noteOn = pyqtSignal(int) 184 noteOff = pyqtSignal(int) 185 notesOn = pyqtSignal() 186 notesOff = pyqtSignal() 187 188 def __init__(self, parent): 189 QWidget.__init__(self, parent) 190 191 self.fEnabledKeys = [] 192 self.fLastMouseNote = -1 193 self.fStartOctave = 0 194 self.fPcKeybOffset = 2 195 self.fInitalizing = True 196 197 self.fFont = self.font() 198 self.fFont.setFamily("Monospace") 199 self.fFont.setPixelSize(12) 200 self.fFont.setBold(True) 201 202 self.fPixmapNormal = QPixmap(":/bitmaps/kbd_normal.png") 203 self.fPixmapDown = QPixmap(":/bitmaps/kbd_down-blue.png") 204 self.fHighlightColor = kValidColors[0] 205 206 self.fkPcKeyLayout = "qwerty" 207 self.fkPcKeys = kPcKeysLayouts["qwerty"] 208 self.fKey2RectMap = kMidiKey2RectMapHorizontal 209 210 self.fWidth = self.fPixmapNormal.width() 211 self.fHeight = self.fPixmapNormal.height() 212 213 self.setCursor(Qt.PointingHandCursor) 214 self.setStartOctave(0) 215 self.setOctaves(6) 216 217 self.loadSettings() 218 219 self.fInitalizing = False 220 221 def saveSettings(self): 222 if self.fInitalizing: 223 return 224 225 settings = QSafeSettings("falkTX", "CarlaKeyboard") 226 settings.setValue("PcKeyboardLayout", self.fkPcKeyLayout) 227 settings.setValue("PcKeyboardOffset", self.fPcKeybOffset) 228 settings.setValue("HighlightColor", self.fHighlightColor) 229 del settings 230 231 def loadSettings(self): 232 settings = QSafeSettings("falkTX", "CarlaKeyboard") 233 self.setPcKeyboardLayout(settings.value("PcKeyboardLayout", self.fkPcKeyLayout, str)) 234 self.setPcKeyboardOffset(settings.value("PcKeyboardOffset", self.fPcKeybOffset, int)) 235 self.setColor(settings.value("HighlightColor", self.fHighlightColor, str)) 236 del settings 237 238 def allNotesOff(self, sendSignal=True): 239 self.fEnabledKeys = [] 240 241 if sendSignal: 242 self.notesOff.emit() 243 244 self.update() 245 246 def sendNoteOn(self, note, sendSignal=True): 247 if 0 <= note <= 127 and note not in self.fEnabledKeys: 248 self.fEnabledKeys.append(note) 249 250 if sendSignal: 251 self.noteOn.emit(note) 252 253 self.update() 254 255 if len(self.fEnabledKeys) == 1: 256 self.notesOn.emit() 257 258 def sendNoteOff(self, note, sendSignal=True): 259 if 0 <= note <= 127 and note in self.fEnabledKeys: 260 self.fEnabledKeys.remove(note) 261 262 if sendSignal: 263 self.noteOff.emit(note) 264 265 self.update() 266 267 if len(self.fEnabledKeys) == 0: 268 self.notesOff.emit() 269 270 def setColor(self, color): 271 if color not in kValidColors: 272 return 273 274 if self.fHighlightColor == color: 275 return 276 277 self.fHighlightColor = color 278 self.fPixmapDown.load(":/bitmaps/kbd_down-{}.png".format(color.lower())) 279 self.saveSettings() 280 281 def setPcKeyboardLayout(self, layout): 282 if layout not in kPcKeysLayouts.keys(): 283 return 284 285 if self.fkPcKeyLayout == layout: 286 return 287 288 self.fkPcKeyLayout = layout 289 self.fkPcKeys = kPcKeysLayouts[layout] 290 self.saveSettings() 291 292 def setPcKeyboardOffset(self, offset): 293 if offset < 0: 294 offset = 0 295 elif offset > 9: 296 offset = 9 297 298 if self.fPcKeybOffset == offset: 299 return 300 301 self.fPcKeybOffset = offset 302 self.saveSettings() 303 304 def setOctaves(self, octaves): 305 if octaves < 1: 306 octaves = 1 307 elif octaves > 10: 308 octaves = 10 309 310 self.fOctaves = octaves 311 312 self.setMinimumSize(self.fWidth * self.fOctaves, self.fHeight) 313 self.setMaximumSize(self.fWidth * self.fOctaves, self.fHeight) 314 315 def setStartOctave(self, octave): 316 if octave < 0: 317 octave = 0 318 elif octave > 9: 319 octave = 9 320 321 if self.fStartOctave == octave: 322 return 323 324 self.fStartOctave = octave 325 self.update() 326 327 def handleMousePos(self, pos): 328 if pos.x() < 0 or pos.x() > self.fOctaves * self.fWidth: 329 return 330 octave = int(pos.x() / self.fWidth) 331 keyPos = QPointF(pos.x() % self.fWidth, pos.y()) 332 333 if self.fKey2RectMap[1].contains(keyPos): # C# 334 note = 1 335 elif self.fKey2RectMap[ 3].contains(keyPos): # D# 336 note = 3 337 elif self.fKey2RectMap[ 6].contains(keyPos): # F# 338 note = 6 339 elif self.fKey2RectMap[ 8].contains(keyPos): # G# 340 note = 8 341 elif self.fKey2RectMap[10].contains(keyPos): # A# 342 note = 10 343 elif self.fKey2RectMap[ 0].contains(keyPos): # C 344 note = 0 345 elif self.fKey2RectMap[ 2].contains(keyPos): # D 346 note = 2 347 elif self.fKey2RectMap[ 4].contains(keyPos): # E 348 note = 4 349 elif self.fKey2RectMap[ 5].contains(keyPos): # F 350 note = 5 351 elif self.fKey2RectMap[ 7].contains(keyPos): # G 352 note = 7 353 elif self.fKey2RectMap[ 9].contains(keyPos): # A 354 note = 9 355 elif self.fKey2RectMap[11].contains(keyPos): # B 356 note = 11 357 else: 358 note = -1 359 360 if note != -1: 361 note += (self.fStartOctave + octave) * 12 362 363 if self.fLastMouseNote != note: 364 self.sendNoteOff(self.fLastMouseNote) 365 self.sendNoteOn(note) 366 367 elif self.fLastMouseNote != -1: 368 self.sendNoteOff(self.fLastMouseNote) 369 370 self.fLastMouseNote = note 371 372 def showOptions(self, event): 373 event.accept() 374 menu = QMenu() 375 376 menu.addAction(self.tr("Note: restart carla to apply globally")).setEnabled(False) 377 menu.addAction(self.tr("Color")).setSeparator(True) 378 379 groupColor = QActionGroup(menu) 380 groupLayout = QActionGroup(menu) 381 actColors = [] 382 actLayouts = [] 383 384 menu.addAction(self.tr("Highlight color")).setSeparator(True) 385 386 for color in kValidColors: 387 act = menu.addAction(color) 388 act.setActionGroup(groupColor) 389 act.setCheckable(True) 390 if self.fHighlightColor == color: 391 act.setChecked(True) 392 actColors.append(act) 393 394 menu.addAction(self.tr("PC Keyboard layout")).setSeparator(True) 395 396 for pcKeyLayout in kPcKeysLayouts.keys(): 397 act = menu.addAction(pcKeyLayout) 398 act.setActionGroup(groupLayout) 399 act.setCheckable(True) 400 if self.fkPcKeyLayout == pcKeyLayout: 401 act.setChecked(True) 402 actLayouts.append(act) 403 404 menu.addAction(self.tr("PC Keyboard base octave (%i)" % self.fPcKeybOffset)).setSeparator(True) 405 406 actOctaveUp = menu.addAction(self.tr("Octave up")) 407 actOctaveDown = menu.addAction(self.tr("Octave down")) 408 409 if self.fPcKeybOffset == 0: 410 actOctaveDown.setEnabled(False) 411 412 actSelected = menu.exec_(event.screenPos().toPoint()) 413 414 if not actSelected: 415 return 416 417 if actSelected in actColors: 418 return self.setColor(actSelected.text()) 419 420 if actSelected in actLayouts: 421 return self.setPcKeyboardLayout(actSelected.text()) 422 423 if actSelected == actOctaveUp: 424 return self.setPcKeyboardOffset(self.fPcKeybOffset + 1) 425 426 if actSelected == actOctaveDown: 427 return self.setPcKeyboardOffset(self.fPcKeybOffset - 1) 428 429 def minimumSizeHint(self): 430 return QSize(self.fWidth, self.fHeight) 431 432 def sizeHint(self): 433 return QSize(self.fWidth * self.fOctaves, self.fHeight) 434 435 def keyPressEvent(self, event): 436 if not event.isAutoRepeat(): 437 try: 438 qKey = str(event.key()) 439 index = self.fkPcKeys.index(qKey) 440 except: 441 pass 442 else: 443 self.sendNoteOn(index+(self.fPcKeybOffset*12)) 444 445 QWidget.keyPressEvent(self, event) 446 447 def keyReleaseEvent(self, event): 448 if not event.isAutoRepeat(): 449 try: 450 qKey = str(event.key()) 451 index = self.fkPcKeys.index(qKey) 452 except: 453 pass 454 else: 455 self.sendNoteOff(index+(self.fPcKeybOffset*12)) 456 457 QWidget.keyReleaseEvent(self, event) 458 459 def mousePressEvent(self, event): 460 if event.button() == Qt.RightButton: 461 self.showOptions(event) 462 else: 463 self.fLastMouseNote = -1 464 self.handleMousePos(event.pos()) 465 self.setFocus() 466 QWidget.mousePressEvent(self, event) 467 468 def mouseMoveEvent(self, event): 469 if event.button() != Qt.RightButton: 470 self.handleMousePos(event.pos()) 471 QWidget.mouseMoveEvent(self, event) 472 473 def mouseReleaseEvent(self, event): 474 if self.fLastMouseNote != -1: 475 self.sendNoteOff(self.fLastMouseNote) 476 self.fLastMouseNote = -1 477 QWidget.mouseReleaseEvent(self, event) 478 479 def paintEvent(self, event): 480 painter = QPainter(self) 481 event.accept() 482 483 # ------------------------------------------------------------- 484 # Paint clean keys (as background) 485 486 for octave in range(self.fOctaves): 487 target = QRectF(self.fWidth * octave, 0, self.fWidth, self.fHeight) 488 source = QRectF(0, 0, self.fWidth, self.fHeight) 489 painter.drawPixmap(target, self.fPixmapNormal, source) 490 491 if not self.isEnabled(): 492 painter.setBrush(QColor(0, 0, 0, 150)) 493 painter.setPen(QColor(0, 0, 0, 150)) 494 painter.drawRect(0, 0, self.width(), self.height()) 495 return 496 497 # ------------------------------------------------------------- 498 # Paint (white) pressed keys 499 500 paintedWhite = False 501 502 for note in self.fEnabledKeys: 503 pos = self._getRectFromMidiNote(note) 504 505 if self._isNoteBlack(note): 506 continue 507 508 if note < 12: 509 octave = 0 510 elif note < 24: 511 octave = 1 512 elif note < 36: 513 octave = 2 514 elif note < 48: 515 octave = 3 516 elif note < 60: 517 octave = 4 518 elif note < 72: 519 octave = 5 520 elif note < 84: 521 octave = 6 522 elif note < 96: 523 octave = 7 524 elif note < 108: 525 octave = 8 526 elif note < 120: 527 octave = 9 528 elif note < 132: 529 octave = 10 530 else: 531 # cannot paint this note 532 continue 533 534 octave -= self.fStartOctave 535 536 target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height()) 537 source = QRectF(pos.x(), 0, pos.width(), pos.height()) 538 539 paintedWhite = True 540 painter.drawPixmap(target, self.fPixmapDown, source) 541 542 # ------------------------------------------------------------- 543 # Clear white keys border 544 545 if paintedWhite: 546 for octave in range(self.fOctaves): 547 for note in kBlackNotes: 548 pos = self._getRectFromMidiNote(note) 549 550 target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height()) 551 source = QRectF(pos.x(), 0, pos.width(), pos.height()) 552 553 painter.drawPixmap(target, self.fPixmapNormal, source) 554 555 # ------------------------------------------------------------- 556 # Paint (black) pressed keys 557 558 for note in self.fEnabledKeys: 559 pos = self._getRectFromMidiNote(note) 560 561 if not self._isNoteBlack(note): 562 continue 563 564 if note < 12: 565 octave = 0 566 elif note < 24: 567 octave = 1 568 elif note < 36: 569 octave = 2 570 elif note < 48: 571 octave = 3 572 elif note < 60: 573 octave = 4 574 elif note < 72: 575 octave = 5 576 elif note < 84: 577 octave = 6 578 elif note < 96: 579 octave = 7 580 elif note < 108: 581 octave = 8 582 elif note < 120: 583 octave = 9 584 elif note < 132: 585 octave = 10 586 else: 587 # cannot paint this note 588 continue 589 590 octave -= self.fStartOctave 591 592 target = QRectF(pos.x() + (self.fWidth * octave), 0, pos.width(), pos.height()) 593 source = QRectF(pos.x(), 0, pos.width(), pos.height()) 594 595 painter.drawPixmap(target, self.fPixmapDown, source) 596 597 # Paint C-number note info 598 painter.setFont(self.fFont) 599 painter.setPen(Qt.black) 600 601 for i in range(self.fOctaves): 602 octave = self.fStartOctave + i - 1 603 painter.drawText(i * 168 + (4 if octave == -1 else 3), 604 35, 20, 20, 605 Qt.AlignCenter, 606 "C{}".format(octave)) 607 608 def _isNoteBlack(self, note): 609 baseNote = note % 12 610 return bool(baseNote in kBlackNotes) 611 612 def _getRectFromMidiNote(self, note): 613 baseNote = note % 12 614 return self.fKey2RectMap[baseNote] 615 616# ------------------------------------------------------------------------------------------------------------ 617# Horizontal scroll area for keyboard 618 619class PixmapKeyboardHArea(QScrollArea): 620 def __init__(self, parent): 621 QScrollArea.__init__(self, parent) 622 623 self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOn) 624 self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 625 626 self.keyboard = PixmapKeyboard(self) 627 self.keyboard.setOctaves(10) 628 self.setWidget(self.keyboard) 629 630 self.setEnabled(False) 631 self.setFixedHeight(int(self.keyboard.height() + self.horizontalScrollBar().height()/2 + 2)) 632 633 QTimer.singleShot(0, self.slot_initScrollbarValue) 634 635 # FIXME use change event 636 def setEnabled(self, yesNo): 637 self.keyboard.setEnabled(yesNo) 638 QScrollArea.setEnabled(self, yesNo) 639 640 @pyqtSlot() 641 def slot_initScrollbarValue(self): 642 self.horizontalScrollBar().setValue(int(self.horizontalScrollBar().maximum()/2)) 643 644# ------------------------------------------------------------------------------------------------------------ 645# Main Testing 646 647if __name__ == '__main__': 648 import sys 649 from PyQt5.QtWidgets import QApplication 650 import resources_rc 651 652 app = QApplication(sys.argv) 653 654 gui = PixmapKeyboard(None) 655 gui.setEnabled(True) 656 gui.show() 657 658 sys.exit(app.exec_()) 659