You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
474 lines
18 KiB
474 lines
18 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2022, 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
|
|
|
|
#Template Modules
|
|
from template.engine.duration import baseDurationToTraditionalNumber
|
|
|
|
#User modules
|
|
import engine.api as api
|
|
from .instrument import GuiInstrument, GuiLibrary #for the types
|
|
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
|
|
|
|
|
|
def currentTreeItemChanged(self, currentTreeItem:QtWidgets.QTreeWidgetItem):
|
|
"""
|
|
Program wide GUI-only callback from
|
|
widget.currentItemChanged->mainWindow.currentTreeItemChanged. We set the currentItem
|
|
ourselves, so we need to block our signals to avoid recursion.
|
|
|
|
Only one item can be selected at a time.
|
|
|
|
The currentTreeItem we receive is not a global instance but from a widget different to ours.
|
|
We need to find our local version of the same instrument/library/idKey first.
|
|
"""
|
|
|
|
isLibrary = type(currentTreeItem) is GuiLibrary
|
|
idKey = currentTreeItem.idKey
|
|
|
|
if isLibrary:
|
|
self.pianoScene.selectedInstrumentChanged(None)
|
|
else:
|
|
self.pianoScene.selectedInstrumentChanged(currentTreeItem.cachedInstrumentStatus)
|
|
|
|
|
|
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("#999999") #grey
|
|
self.setBackgroundBrush(self.backColor)
|
|
|
|
self.linesHorizontal = []
|
|
self.allKeys = {} # pitch/int : BlackKey or WhiteKey
|
|
self.numberLabels = [] #index is pitch
|
|
|
|
self._selectedInstrument = None #instrumentStatus dict
|
|
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.7)
|
|
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["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["keyLabels"]:
|
|
self.numberLabels[keyPitch].setLabel(instrumentStatus["keyLabels"][keyPitch], keyswitch=False) #can be overwritten by keyswitch label. otherwise on any key, no matter if deactivated or not
|
|
|
|
if keyPitch in instrumentStatus["keySwitches"]:
|
|
opcode, keyswitchLabel = instrumentStatus["keySwitches"][keyPitch]
|
|
keyObject.setPlayable(True)
|
|
keyObject.setKeySwitch(True)
|
|
self.numberLabels[keyPitch].setLabel(keyswitchLabel, keyswitch=True)
|
|
|
|
elif keyPitch in instrumentStatus["playableKeys"]:
|
|
keyObject.setPlayable(True)
|
|
#else:
|
|
#self.numberLabels[keyPitch].hide()
|
|
|
|
|
|
def selectedInstrumentChanged(self, instrumentStatus):
|
|
"""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
|
|
self.instrumentStatusChanged(instrumentStatus)
|
|
|
|
def highlightNoteOn(self, idKey:tuple, pitch:int, velocity:int):
|
|
if self._selectedInstrument and self._selectedInstrument["idKey"] == idKey:
|
|
self.allKeys[pitch].highlightOn()
|
|
|
|
def highlightNoteOff(self, idKey:tuple, pitch:int, velocity:int):
|
|
if self._selectedInstrument and self._selectedInstrument["idKey"] == idKey:
|
|
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)
|
|
else:
|
|
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 = self._selectedInstrument
|
|
libId, instrId = status["idKey"]
|
|
api.sendNoteOffToInstrument(status["idKey"], self._lastPlayPitch)
|
|
self._lastPlayPitch = None
|
|
|
|
def _play(self, event):
|
|
assert self._leftMouseDown
|
|
|
|
potentialItems = self.items(event.scenePos() )
|
|
|
|
for potentialItem in potentialItems:
|
|
if type(potentialItem) in (BlackKey, WhiteKey):
|
|
break
|
|
else:
|
|
return #no item found.
|
|
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 = 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, parentScene, number:int):
|
|
super().__init__()
|
|
self.parentScene = parentScene
|
|
self.number = number
|
|
self.labelItem = QtWidgets.QGraphicsSimpleTextItem("")
|
|
self.labelItem.setParentItem(self)
|
|
self.labelItem.setEnabled(False)
|
|
self.currentLabel = ""
|
|
self.setText(str(number))
|
|
self.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True)
|
|
self.setEnabled(False) #Ignored?
|
|
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
|
|
|
|
self.blackKey = number % 12 in (1, 3, 6, 8, 10)
|
|
if self.blackKey:
|
|
self.setScale(0.9)
|
|
self.labelItem.setRotation(90)
|
|
self.labelItem.setPos(15,15)
|
|
else:
|
|
self.setScale(1)
|
|
self.labelItem.setRotation(-90)
|
|
self.labelItem.setPos(-5,0)
|
|
|
|
self.setTextColor(blackKey=self.blackKey, keyswitch=False)
|
|
|
|
def setTextColor(self, blackKey:bool, keyswitch:bool):
|
|
if blackKey and not keyswitch:
|
|
self.setBrush(QtGui.QColor("white"))
|
|
self.labelItem.setBrush(QtGui.QColor("white"))
|
|
else:
|
|
self.setBrush(QtGui.QColor("black"))
|
|
self.labelItem.setBrush(QtGui.QColor("black"))
|
|
|
|
def setLabel(self, label:str, keyswitch=False):
|
|
"""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)
|
|
self.setTextColor(blackKey=self.blackKey, keyswitch=keyswitch)
|
|
|
|
|
|
class BlackKey(QtWidgets.QGraphicsRectItem):
|
|
def __init__(self, parentScene, pitch):
|
|
super().__init__(0, 0, WIDTH * 0.8, HEIGHT) #x, y, w, h
|
|
self.parentScene = parentScene
|
|
self.pitch = pitch
|
|
self.state = False
|
|
self.setPen(QtGui.QPen(QtCore.Qt.NoPen))
|
|
self.setBrush(QtGui.QColor("black"))
|
|
self.setAcceptHoverEvents(True)
|
|
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.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):
|
|
c = QtGui.QColor()
|
|
self.state = state
|
|
if state:
|
|
c.setNamedColor("#0c0c0c")
|
|
else:
|
|
#Only if the instrument is activated. Not loaded instruments are just dark black and white
|
|
c.setNamedColor("#444444")
|
|
self.setBrush(c)
|
|
|
|
def hoverEnterEvent(self, event):
|
|
if self.state:
|
|
l = self.parentScene.numberLabels[self.pitch].currentLabel
|
|
if l:
|
|
self.parentScene.parentView.mainWindow.statusBar().showMessage(f"[{self.pitch}] {l}")
|
|
|
|
def hoverLeaveEvent(self, event):
|
|
self.parentScene.parentView.mainWindow.statusBar().showMessage("")
|
|
|
|
def setKeySwitch(self, state:bool):
|
|
self.state = state
|
|
if state:
|
|
self.decorationOverlay.show()
|
|
self.decorationOverlay.setBrush(QtGui.QColor("orange"))
|
|
else:
|
|
self.decorationOverlay.hide()
|
|
|
|
def highlightOn(self):
|
|
self.highlight.show()
|
|
|
|
def highlightOff(self):
|
|
self.highlight.hide()
|
|
|
|
class WhiteKey(QtWidgets.QGraphicsRectItem):
|
|
def __init__(self, parentScene, pitch:int):
|
|
super().__init__(0, 0, WIDTH, HEIGHT) #x, y, w, h
|
|
self.parentScene = parentScene
|
|
self.pitch = pitch
|
|
self.state = False
|
|
self.setPen(QtGui.QPen(QtCore.Qt.NoPen))
|
|
self.setBrush(QtGui.QColor("white"))
|
|
self.setAcceptHoverEvents(True)
|
|
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
|
|
|
|
self.decorationOverlay = QtWidgets.QGraphicsRectItem(0, 0, WIDTH, HEIGHT) #x, y, w, h
|
|
self.decorationOverlay.setParentItem(self)
|
|
self.decorationOverlay.setEnabled(False)
|
|
self.decorationOverlay.setAcceptedMouseButtons(QtCore.Qt.NoButton)
|
|
self.decorationOverlay.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True)
|
|
|
|
self.highlight = QtWidgets.QGraphicsRectItem(0, 0, WIDTH, HEIGHT) #x, y, w, h
|
|
self.highlight.setParentItem(self)
|
|
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()
|
|
|
|
|
|
def hoverEnterEvent(self, event):
|
|
if self.state:
|
|
l = self.parentScene.numberLabels[self.pitch].currentLabel
|
|
if l:
|
|
self.parentScene.parentView.mainWindow.statusBar().showMessage(f"[{self.pitch}] {l}")
|
|
|
|
def hoverLeaveEvent(self, event):
|
|
self.parentScene.parentView.mainWindow.statusBar().showMessage("")
|
|
|
|
def setPlayable(self, state:bool):
|
|
c = QtGui.QColor()
|
|
self.state = state
|
|
if state:
|
|
c.setNamedColor("#fdfdff")
|
|
else:
|
|
#Only if the instrument is activated. Not loaded instruments are just dark black and white
|
|
c.setNamedColor("#999999")
|
|
self.setBrush(c)
|
|
|
|
def setKeySwitch(self, state:bool):
|
|
self.state = state
|
|
if state:
|
|
self.decorationOverlay.show()
|
|
self.decorationOverlay.setBrush(QtGui.QColor("orange"))
|
|
else:
|
|
self.decorationOverlay.hide()
|
|
|
|
def highlightOn(self):
|
|
self.highlight.show()
|
|
|
|
def highlightOff(self):
|
|
self.highlight.hide()
|
|
|