Nils
3 years ago
6 changed files with 437 additions and 35 deletions
@ -0,0 +1,405 @@ |
|||||
|
#! /usr/bin/env python3 |
||||
|
# -*- coding: utf-8 -*- |
||||
|
""" |
||||
|
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) |
||||
|
|
||||
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), |
||||
|
|
||||
|
This 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 3 of the License, or |
||||
|
(at your option) 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. |
||||
|
|
||||
|
You should have received a copy of the GNU General Public License |
||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
""" |
||||
|
import logging; logger = logging.getLogger(__name__); logger.info("import") |
||||
|
|
||||
|
#Standard Library Modules |
||||
|
|
||||
|
#Third Party Modules |
||||
|
from PyQt5 import QtWidgets, QtCore, QtGui, QtOpenGL |
||||
|
|
||||
|
#Template Modules |
||||
|
from template.engine.duration import baseDurationToTraditionalNumber |
||||
|
|
||||
|
#User modules |
||||
|
import engine.api as api |
||||
|
from .verticalpiano import WIDTH as HEIGHT |
||||
|
from .verticalpiano import STAFFLINEGAP as WIDTH |
||||
|
WIDTH = WIDTH * 1.5 |
||||
|
SCOREWIDTH = WIDTH * 75 #75 white keys. The vertical piano does have a linear layout while we have the inverleaved piano one. |
||||
|
|
||||
|
|
||||
|
class HorizontalPiano(QtWidgets.QGraphicsView): |
||||
|
def __init__(self, mainWindow): |
||||
|
super().__init__(mainWindow) |
||||
|
self.mainWindow = mainWindow |
||||
|
|
||||
|
self.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop) |
||||
|
self.setDragMode(QtWidgets.QGraphicsView.NoDrag) |
||||
|
|
||||
|
self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) |
||||
|
self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) |
||||
|
|
||||
|
self.pianoScene = _HorizontalPianoScene(self) |
||||
|
self.setScene(self.pianoScene) |
||||
|
|
||||
|
self.setSceneRect(QtCore.QRectF(0, 0, SCOREWIDTH, HEIGHT)) #x,y,w,h |
||||
|
#self.setFixedHeight(SCOREHEIGHT+3) # Don't set to scoreheight. Then we don't get a scrollbar. We need to set the sceneRect to 100%, not the view. |
||||
|
#self.mainWindow.ui.horizontalPianoFrame.setFixedHeight(SCOREHEIGHT+3) #Don't. This makes the whole window a fixed size! |
||||
|
#self.setFixedWidth(WIDTH) #Also done by parent widget in mainWindow |
||||
|
self.setLineWidth(0) |
||||
|
|
||||
|
self.centerOn(SCOREWIDTH/2, 0) |
||||
|
|
||||
|
def wheelEvent(self, event): |
||||
|
"""Convert vertical scrolling to horizontal""" |
||||
|
event.accept() #eat the event |
||||
|
self.horizontalScrollBar().setValue(self.horizontalScrollBar().value() + event.pixelDelta().y()) #y because it is the original vert. scroll |
||||
|
|
||||
|
|
||||
|
class _HorizontalPianoScene(QtWidgets.QGraphicsScene): |
||||
|
"""Most of this is copy paste from piano grid""" |
||||
|
|
||||
|
def __init__(self, parentView): |
||||
|
super().__init__() |
||||
|
|
||||
|
self.parentView = parentView |
||||
|
#Set color, otherwise it will be transparent in window managers or wayland that want that. |
||||
|
self.backColor = QtGui.QColor() |
||||
|
self.backColor.setNamedColor("#fdfdff") |
||||
|
self.setBackgroundBrush(self.backColor) |
||||
|
|
||||
|
self.linesHorizontal = [] |
||||
|
self.allKeys = {} # pitch/int : BlackKey or WhiteKey |
||||
|
self.numberLabels = [] #index is pitch |
||||
|
|
||||
|
self._selectedInstrument = None #tuple instrumentStatus, instrumentData |
||||
|
self._leftMouseDown = False #For note preview |
||||
|
|
||||
|
self.gridPen = QtGui.QPen(QtCore.Qt.SolidLine) |
||||
|
self.gridPen.setCosmetic(True) |
||||
|
|
||||
|
boldPen = QtGui.QPen(QtCore.Qt.SolidLine) |
||||
|
boldPen.setCosmetic(True) |
||||
|
boldPen.setWidth(1) |
||||
|
|
||||
|
whitekeyCounter = 0 |
||||
|
|
||||
|
for i in range(128): |
||||
|
blackKey = i % 12 in (1, 3, 6, 8, 10) |
||||
|
if blackKey: |
||||
|
key = BlackKey(self, i) |
||||
|
x = whitekeyCounter * WIDTH - WIDTH/2 |
||||
|
numberY = 0 #for later |
||||
|
if i < 100: |
||||
|
numberX = x + 4 |
||||
|
else: |
||||
|
numberX = x |
||||
|
key.setPos(x, HEIGHT * -0.66) |
||||
|
key.setZValue(4) |
||||
|
else: |
||||
|
key = WhiteKey(self, i) |
||||
|
x = whitekeyCounter * WIDTH |
||||
|
key.setPos(x, 0) |
||||
|
key.setZValue(1) |
||||
|
if i < 100: |
||||
|
numberX = x + 5 |
||||
|
else: |
||||
|
numberX = x + 3 |
||||
|
numberY = HEIGHT/2 -3 #100 #for later |
||||
|
whitekeyCounter += 1 |
||||
|
|
||||
|
self.addItem(key) #we can setPos before adding to the scene. |
||||
|
self.allKeys[i] = key |
||||
|
|
||||
|
if not blackKey: |
||||
|
vline = QtWidgets.QGraphicsLineItem(0, 0, 0, HEIGHT) #x1, y1, x2, y2 |
||||
|
vline.setPen(self.gridPen) |
||||
|
self.addItem(vline) |
||||
|
vline.setPos(key.x(), 0) |
||||
|
vline.setEnabled(False) |
||||
|
vline.setAcceptedMouseButtons(QtCore.Qt.NoButton) #Disabled items discard the mouse event unless mouseButtons are not accepted |
||||
|
vline.setZValue(2) #above the white keys |
||||
|
self.linesHorizontal.append(vline) |
||||
|
|
||||
|
|
||||
|
#Numbers last so they are on top. |
||||
|
numberLabel = NumberLabel(self, i) |
||||
|
self.addItem(numberLabel) |
||||
|
self.numberLabels.append(numberLabel) #index is pitch |
||||
|
#numberLabel.setPos(i * WIDTH + 2, 0) |
||||
|
numberLabel.setPos(numberX, numberY) |
||||
|
numberLabel.setZValue(10) |
||||
|
|
||||
|
self.fakeDeactivationOverlay = QtWidgets.QGraphicsRectItem(0,0, SCOREWIDTH, HEIGHT) |
||||
|
self.fakeDeactivationOverlay.setBrush(QtGui.QColor("black")) |
||||
|
self.fakeDeactivationOverlay.setOpacity(0.6) |
||||
|
self.fakeDeactivationOverlay.setEnabled(False) |
||||
|
self.fakeDeactivationOverlay.setAcceptedMouseButtons(QtCore.Qt.NoButton) #Disabled items discard the mouse event unless mouseButtons are not accepted |
||||
|
self.addItem(self.fakeDeactivationOverlay) |
||||
|
self.fakeDeactivationOverlay.setPos(0,0) |
||||
|
self.fakeDeactivationOverlay.setZValue(12) #above the numbers |
||||
|
self.fakeDeactivationOverlay.show() |
||||
|
|
||||
|
#Keyboard Creation Done |
||||
|
api.callbacks.instrumentMidiNoteOnActivity.append(self.highlightNoteOn) |
||||
|
api.callbacks.instrumentMidiNoteOffActivity.append(self.highlightNoteOff) |
||||
|
api.callbacks.instrumentStatusChanged.append(self.instrumentStatusChanged) |
||||
|
|
||||
|
def clearHorizontalPiano(self): |
||||
|
for keyPitch, keyObject in self.allKeys.items(): |
||||
|
keyObject.show() |
||||
|
keyObject.highlightOff() |
||||
|
keyObject.setPlayable(False) |
||||
|
keyObject.setKeySwitch(False) |
||||
|
|
||||
|
for nl in self.numberLabels: |
||||
|
nl.setLabel("") #reset to just number |
||||
|
self.fakeDeactivationOverlay.show() |
||||
|
|
||||
|
def instrumentStatusChanged(self, instrumentStatus:dict): |
||||
|
"""GUI callback. Data is live""" |
||||
|
#Is this for us? |
||||
|
|
||||
|
if instrumentStatus and self._selectedInstrument and not instrumentStatus["idKey"] == self._selectedInstrument[0]["idKey"]: |
||||
|
return |
||||
|
#else: |
||||
|
# print ("not for us", instrumentStatus["idKey"]) |
||||
|
|
||||
|
self.clearHorizontalPiano() |
||||
|
if not instrumentStatus["state"]: |
||||
|
self.fakeDeactivationOverlay.show() |
||||
|
return |
||||
|
|
||||
|
self.fakeDeactivationOverlay.hide() |
||||
|
|
||||
|
for keyPitch, keyObject in self.allKeys.items(): |
||||
|
if keyPitch in instrumentStatus["keySwitches"]: |
||||
|
opcode, keyswitchLabel = instrumentStatus["keySwitches"][keyPitch] |
||||
|
self.numberLabels[keyPitch].setLabel(keyswitchLabel) |
||||
|
keyObject.setKeySwitch(True) |
||||
|
elif keyPitch in instrumentStatus["playableKeys"]: |
||||
|
keyObject.setPlayable(True) |
||||
|
#else: |
||||
|
#self.numberLabels[keyPitch].hide() |
||||
|
|
||||
|
|
||||
|
def selectedInstrumentChanged(self, instrumentStatus, instrumentData): |
||||
|
"""GUI click to different instrument. The arguments are cached GUI data |
||||
|
|
||||
|
If a library is clicked, and not an instrument, both parameters will be None. |
||||
|
""" |
||||
|
if instrumentStatus is None: |
||||
|
self._selectedInstrument = None |
||||
|
self.clearHorizontalPiano() |
||||
|
self.fakeDeactivationOverlay.show() |
||||
|
else: |
||||
|
self._selectedInstrument = (instrumentStatus, instrumentData) |
||||
|
self.instrumentStatusChanged(instrumentStatus) |
||||
|
|
||||
|
def highlightNoteOn(self, idKey:tuple, pitch:int, velocity:int): |
||||
|
self.allKeys[pitch].highlightOn() |
||||
|
|
||||
|
def highlightNoteOff(self, idKey:tuple, pitch:int, velocity:int): |
||||
|
self.allKeys[pitch].highlightOff() |
||||
|
|
||||
|
def allHighlightsOff(self): |
||||
|
for keyPitch, keyObject in self.allKeys.items(): |
||||
|
keyObject[pitch].highlightNoteOff() |
||||
|
|
||||
|
def mousePressEvent(self, event): |
||||
|
self._leftMouseDown = False |
||||
|
if event.button() == QtCore.Qt.LeftButton: |
||||
|
self._leftMouseDown = True |
||||
|
self._lastPlayPitch = None #this must not be in _play, otherwise you can't move the mouse while pressed down |
||||
|
self._play(event) |
||||
|
super().mousePressEvent(event) |
||||
|
|
||||
|
def wheelEvent(self, event): |
||||
|
event.ignore() #let the view handle it, for the scrollbar |
||||
|
|
||||
|
|
||||
|
def _off(self): |
||||
|
if self._selectedInstrument and not self._lastPlayPitch is None: |
||||
|
status, data = self._selectedInstrument |
||||
|
libId, instrId = status["idKey"] |
||||
|
api.sendNoteOffToInstrument(status["idKey"], self._lastPlayPitch) |
||||
|
self._lastPlayPitch = None |
||||
|
|
||||
|
def _play(self, event): |
||||
|
assert self._leftMouseDown |
||||
|
|
||||
|
potentialItem = self.itemAt(event.scenePos(), self.parentView.transform() ) |
||||
|
if not (potentialItem and type(potentialItem) in (BlackKey, WhiteKey)): |
||||
|
return |
||||
|
pitch = potentialItem.pitch |
||||
|
|
||||
|
|
||||
|
if pitch < 0 or pitch > 127: |
||||
|
pitch = None |
||||
|
|
||||
|
if self._selectedInstrument and not pitch == self._lastPlayPitch: |
||||
|
#TODO: Play note on at a different instrument than note off? Possible? |
||||
|
status, data = self._selectedInstrument |
||||
|
|
||||
|
if not self._lastPlayPitch is None: |
||||
|
#Force a note off that is currently playing but not under the cursor anymore |
||||
|
#User did some input tricks with keyboard and mouse combined etc. |
||||
|
api.sendNoteOffToInstrument(status["idKey"], self._lastPlayPitch) |
||||
|
|
||||
|
if not pitch is None: |
||||
|
#This is the normal note-on click |
||||
|
api.sendNoteOnToInstrument(status["idKey"], pitch) |
||||
|
|
||||
|
self._lastPlayPitch = pitch |
||||
|
|
||||
|
def mouseMoveEvent(self, event): |
||||
|
"""Event button is always 0 in a mouse move event""" |
||||
|
if self._leftMouseDown: |
||||
|
self._play(event) |
||||
|
super().mouseMoveEvent(event) |
||||
|
|
||||
|
def mouseReleaseEvent(self, event): |
||||
|
if self._leftMouseDown: |
||||
|
self._off() |
||||
|
self._leftMouseDown = False |
||||
|
if event.button() == QtCore.Qt.LeftButton: |
||||
|
self._lastPlayPitch = None |
||||
|
super().mouseReleaseEvent(event) |
||||
|
|
||||
|
|
||||
|
class NumberLabel(QtWidgets.QGraphicsSimpleTextItem): |
||||
|
"""In opposite to verticalPiano the key label is a different childItem as the number""" |
||||
|
|
||||
|
def __init__(self, parentGrid, number:int): |
||||
|
super().__init__() |
||||
|
self.parentGrid = parentGrid |
||||
|
self.number = number |
||||
|
self.labelItem = QtWidgets.QGraphicsSimpleTextItem("") |
||||
|
self.labelItem.setParentItem(self) |
||||
|
self.currentLabel = "" |
||||
|
self.setText(str(number)) |
||||
|
self.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True) |
||||
|
self.setAcceptedMouseButtons(QtCore.Qt.NoButton) |
||||
|
blackKey = number % 12 in (1, 3, 6, 8, 10) |
||||
|
if blackKey: |
||||
|
self.setBrush(QtGui.QColor("white")) |
||||
|
self.labelItem.setBrush(QtGui.QColor("white")) |
||||
|
self.setScale(0.9) |
||||
|
self.labelItem.setRotation(90) |
||||
|
self.labelItem.setPos(15,15) |
||||
|
else: |
||||
|
self.setBrush(QtGui.QColor("black")) |
||||
|
self.labelItem.setBrush(QtGui.QColor("black")) |
||||
|
self.setScale(1) |
||||
|
self.labelItem.setRotation(-90) |
||||
|
self.labelItem.setPos(-5,0) |
||||
|
|
||||
|
def setLabel(self, label:str): |
||||
|
"""Each key can have an optional text label for keyswitches, percussion names etc. |
||||
|
Use with empty string to reset to just the midi pitch number.""" |
||||
|
self.currentLabel = label |
||||
|
self.labelItem.setText(label) |
||||
|
|
||||
|
|
||||
|
class BlackKey(QtWidgets.QGraphicsRectItem): |
||||
|
def __init__(self, parentGrid, pitch): |
||||
|
super().__init__(0, 0, WIDTH * 0.8, HEIGHT) #x, y, w, h |
||||
|
self.parentGrid = parentGrid |
||||
|
self.pitch = pitch |
||||
|
self.setPen(QtGui.QPen(QtCore.Qt.NoPen)) |
||||
|
self.setBrush(QtGui.QColor("black")) |
||||
|
self.setEnabled(False) |
||||
|
self.setAcceptedMouseButtons(QtCore.Qt.NoButton) |
||||
|
|
||||
|
self.decorationOverlay = QtWidgets.QGraphicsRectItem(0, 0, WIDTH * 0.8, HEIGHT) #x, y, w, h |
||||
|
self.decorationOverlay.setEnabled(False) |
||||
|
self.decorationOverlay.setAcceptedMouseButtons(QtCore.Qt.NoButton) |
||||
|
self.decorationOverlay.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True) |
||||
|
self.decorationOverlay.setOpacity(0.2) #just a tint |
||||
|
self.decorationOverlay.hide() |
||||
|
self.decorationOverlay.setParentItem(self) |
||||
|
|
||||
|
self.highlight = QtWidgets.QGraphicsRectItem(0, 0, WIDTH * 0.8, HEIGHT) #x, y, w, h |
||||
|
self.highlight.setEnabled(False) |
||||
|
self.highlight.setAcceptedMouseButtons(QtCore.Qt.NoButton) |
||||
|
self.highlight.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True) |
||||
|
self.highlight.setOpacity(0.5) |
||||
|
self.highlight.setBrush(QtGui.QColor("cyan")) |
||||
|
self.highlight.hide() |
||||
|
self.highlight.setParentItem(self) |
||||
|
|
||||
|
def setPlayable(self, state:bool): |
||||
|
if state: |
||||
|
self.decorationOverlay.show() |
||||
|
self.decorationOverlay.setBrush(QtGui.QColor("orange")) |
||||
|
else: |
||||
|
self.decorationOverlay.hide() |
||||
|
|
||||
|
def setKeySwitch(self, state:bool): |
||||
|
if state: |
||||
|
self.decorationOverlay.show() |
||||
|
self.decorationOverlay.setBrush(QtGui.QColor("red")) |
||||
|
else: |
||||
|
self.decorationOverlay.hide() |
||||
|
|
||||
|
def highlightOn(self): |
||||
|
self.highlight.show() |
||||
|
|
||||
|
def highlightOff(self): |
||||
|
self.highlight.hide() |
||||
|
|
||||
|
class WhiteKey(QtWidgets.QGraphicsRectItem): |
||||
|
def __init__(self, parentGrid, pitch:int): |
||||
|
super().__init__(0, 0, WIDTH, HEIGHT) #x, y, w, h |
||||
|
self.parentGrid = parentGrid |
||||
|
self.pitch = pitch |
||||
|
self.setPen(QtGui.QPen(QtCore.Qt.NoPen)) |
||||
|
self.setBrush(QtGui.QColor("white")) |
||||
|
self.setEnabled(False) |
||||
|
self.setAcceptedMouseButtons(QtCore.Qt.NoButton) |
||||
|
|
||||
|
self.decorationOverlay = QtWidgets.QGraphicsRectItem(0, 0, WIDTH, HEIGHT) #x, y, w, h |
||||
|
self.decorationOverlay.setEnabled(False) |
||||
|
self.decorationOverlay.setAcceptedMouseButtons(QtCore.Qt.NoButton) |
||||
|
self.decorationOverlay.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True) |
||||
|
self.decorationOverlay.setOpacity(0.2) #just a tint |
||||
|
self.decorationOverlay.hide() |
||||
|
self.decorationOverlay.setParentItem(self) |
||||
|
|
||||
|
self.highlight = QtWidgets.QGraphicsRectItem(0, 0, WIDTH, HEIGHT) #x, y, w, h |
||||
|
self.highlight.setEnabled(False) |
||||
|
self.highlight.setAcceptedMouseButtons(QtCore.Qt.NoButton) |
||||
|
self.highlight.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True) |
||||
|
self.highlight.setOpacity(0.5) |
||||
|
self.highlight.setBrush(QtGui.QColor("cyan")) |
||||
|
self.highlight.hide() |
||||
|
self.highlight.setParentItem(self) |
||||
|
|
||||
|
|
||||
|
def setPlayable(self, state:bool): |
||||
|
if state: |
||||
|
self.decorationOverlay.show() |
||||
|
self.decorationOverlay.setBrush(QtGui.QColor("orange")) |
||||
|
else: |
||||
|
self.decorationOverlay.hide() |
||||
|
|
||||
|
def setKeySwitch(self, state:bool): |
||||
|
if state: |
||||
|
self.decorationOverlay.show() |
||||
|
self.decorationOverlay.setBrush(QtGui.QColor("red")) |
||||
|
else: |
||||
|
self.decorationOverlay.hide() |
||||
|
|
||||
|
def highlightOn(self): |
||||
|
self.highlight.show() |
||||
|
|
||||
|
def highlightOff(self): |
||||
|
self.highlight.hide() |
Loading…
Reference in new issue