#! /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
WIDTH = 200
STAFFLINEGAP = 20 #cannot be changed during runtime
SCOREHEIGHT = STAFFLINEGAP * 128 #notes
class VerticalPiano ( 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 . ScrollBarAlwaysOff )
self . setVerticalScrollBarPolicy ( QtCore . Qt . ScrollBarAlwaysOn )
self . pianoScene = _VerticalPianoScene ( self )
self . setScene ( self . pianoScene )
self . setSceneRect ( QtCore . QRectF ( 0 , 0 , WIDTH / 2 , SCOREHEIGHT ) ) #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.verticalPianoFrame.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 ( 0 , 64 * STAFFLINEGAP )
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 _VerticalPianoScene ( 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 . backColor . setNamedColor ( " #999999 " ) #grey
self . setBackgroundBrush ( self . backColor )
self . linesHorizontal = [ ]
self . highlights = { }
self . colorKeys = { }
self . blackKeys = [ ]
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 )
#Create two lines for the upper/lower boundaries first. They are just cosmetic
boldPen = QtGui . QPen ( QtCore . Qt . SolidLine )
boldPen . setCosmetic ( True )
boldPen . setWidth ( 1 )
hlineUp = QtWidgets . QGraphicsLineItem ( 0 , 0 , WIDTH * 2 , 0 ) #x1, y1, x2, y2
hlineUp . setPen ( boldPen )
self . addItem ( hlineUp )
hlineUp . setPos ( 0 , 0 )
hlineUp . setEnabled ( False )
hlineUp . setAcceptedMouseButtons ( QtCore . Qt . NoButton )
hlineUp . setFlag ( QtWidgets . QGraphicsItem . ItemIgnoresParentOpacity , True )
self . linesHorizontal . append ( hlineUp )
hlineDown = QtWidgets . QGraphicsLineItem ( 0 , 0 , WIDTH * 2 , 0 ) #x1, y1, x2, y2
hlineDown . setPen ( boldPen )
self . addItem ( hlineDown )
hlineDown . setPos ( 0 , 128 * STAFFLINEGAP )
hlineDown . setEnabled ( False )
hlineDown . setAcceptedMouseButtons ( QtCore . Qt . NoButton )
hlineDown . setFlag ( QtWidgets . QGraphicsItem . ItemIgnoresParentOpacity , True )
self . linesHorizontal . append ( hlineDown )
for i in range ( 128 ) :
hline = QtWidgets . QGraphicsLineItem ( 0 , 0 , WIDTH * 2 , 0 ) #x1, y1, x2, y2
hline . setPen ( self . gridPen )
self . addItem ( hline )
hline . setPos ( 0 , i * STAFFLINEGAP )
hline . setEnabled ( False )
hline . setAcceptedMouseButtons ( QtCore . Qt . NoButton ) #Disabled items discard the mouse event unless mouseButtons are not accepted
self . linesHorizontal . append ( hline )
blackKey = i % 12 in ( 1 , 3 , 6 , 8 , 10 )
if blackKey :
bk = BlackKey ( self )
self . blackKeys . append ( bk )
self . addItem ( bk )
bk . setPos ( 0 , ( 127 - i ) * STAFFLINEGAP )
#Various purpose color keys. They are opaque and are on top of white/black keys
ck = ColorKey ( self , i , QtGui . QColor ( " cyan " ) , blackKey )
self . addItem ( ck )
self . colorKeys [ i ] = ck
ck . setPos ( 0 , ( 127 - i ) * STAFFLINEGAP )
#Highlights on top of colors. Indication if note is played.
hl = Highlight ( self )
self . addItem ( hl )
self . highlights [ i ] = hl
hl . setPos ( 0 , ( 127 - i ) * STAFFLINEGAP )
#Numbers last so they are on top.
numberLabel = NumberLabel ( self , 127 - i )
self . addItem ( numberLabel )
self . numberLabels . append ( numberLabel ) #index is pitch
numberLabel . setPos ( 3 , i * STAFFLINEGAP + 2 )
numberLabel . setZValue ( 10 )
self . numberLabels . reverse ( )
self . fakeDeactivationOverlay = QtWidgets . QGraphicsRectItem ( 0 , 0 , WIDTH , SCOREHEIGHT )
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 . show ( )
#Keyboard Creation Done
api . callbacks . instrumentMidiNoteOnActivity . append ( self . highlightNoteOn )
api . callbacks . instrumentMidiNoteOffActivity . append ( self . highlightNoteOff )
api . callbacks . instrumentStatusChanged . append ( self . instrumentStatusChanged )
def clearVerticalPiano ( self ) :
for colorKeyObj in self . colorKeys . values ( ) :
colorKeyObj . hide ( )
self . allHighlightsOff ( )
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 . clearVerticalPiano ( )
if not instrumentStatus [ " state " ] :
self . fakeDeactivationOverlay . show ( )
return
self . fakeDeactivationOverlay . hide ( )
for keyPitch , keyObject in self . colorKeys . items ( ) :
#self.numberLabels[keyPitch].show()
keyObject . show ( )
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 ]
self . numberLabels [ keyPitch ] . setLabel ( keyswitchLabel , keyswitch = True )
keyObject . setPlayable ( True )
keyObject . setBrush ( QtGui . QColor ( " orange " ) )
else :
#self.numberLabels[keyPitch].hide()
#keyObject.hide()
keyObject . setPlayable ( keyPitch in instrumentStatus [ " playableKeys " ] )
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 . clearVerticalPiano ( )
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 :
highlight = self . highlights [ pitch ]
highlight . show ( )
def highlightNoteOff ( self , idKey : tuple , pitch : int , velocity : int ) :
if self . _selectedInstrument and self . _selectedInstrument [ " idKey " ] == idKey :
highlight = self . highlights [ pitch ]
highlight . hide ( )
def allHighlightsOff ( self ) :
for pitch , highlight in self . highlights . items ( ) :
highlight . hide ( )
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 = self . _selectedInstrument
libId , instrId = status [ " idKey " ]
api . sendNoteOffToInstrument ( status [ " idKey " ] , self . _lastPlayPitch )
self . _lastPlayPitch = None
def _play ( self , event ) :
assert self . _leftMouseDown
pitch = 127 - int ( event . scenePos ( ) . y ( ) / STAFFLINEGAP )
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 ) :
def __init__ ( self , parentPiano , number : int ) :
super ( ) . __init__ ( )
self . parentPiano = parentPiano
self . number = number
self . currentLabel = " "
self . setText ( str ( number ) )
self . setFlag ( QtWidgets . QGraphicsItem . ItemIgnoresParentOpacity , True )
self . setAcceptedMouseButtons ( QtCore . Qt . NoButton )
self . setScale ( 1 )
self . blackKey = number % 12 in ( 1 , 3 , 6 , 8 , 10 )
def setTextColor ( self , blackKey : bool , keyswitch : bool ) :
if blackKey and not keyswitch :
self . setBrush ( QtGui . QColor ( " white " ) )
else :
self . 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 . setText ( f " { self . number } { label } " )
self . setTextColor ( self . blackKey , keyswitch )
class Highlight ( QtWidgets . QGraphicsRectItem ) :
def __init__ ( self , parentPiano ) :
super ( ) . __init__ ( 0 , 0 , WIDTH , STAFFLINEGAP ) #x, y, w, h
self . setEnabled ( False ) #Not clickable, still visible.
self . setAcceptedMouseButtons ( QtCore . Qt . NoButton )
self . setFlag ( QtWidgets . QGraphicsItem . ItemIgnoresParentOpacity , True )
self . setBrush ( QtGui . QColor ( " cyan " ) )
self . setOpacity ( 0.5 )
self . hide ( )
class ColorKey ( QtWidgets . QGraphicsRectItem ) :
""" These are the actual, playable, keys. And key switches """
def __init__ ( self , parentPiano , pitch , color : QtGui . QColor , blackKey : bool ) :
super ( ) . __init__ ( 0 , 0 , WIDTH , STAFFLINEGAP ) #x, y, w, h
self . parentPiano = parentPiano
self . pitch = pitch
self . setEnabled ( True ) #Not clickable, still visible.
self . setAcceptedMouseButtons ( QtCore . Qt . NoButton )
self . setAcceptHoverEvents ( True )
self . setFlag ( QtWidgets . QGraphicsItem . ItemIgnoresParentOpacity , True )
self . setBrush ( color )
self . state = False
self . blackKey = blackKey
self . hide ( )
def setPlayable ( self , state : bool ) :
c = QtGui . QColor ( )
self . state = state
if state :
if self . blackKey :
c . setNamedColor ( " #0c0c0c " )
else :
c . setNamedColor ( " #fdfdff " )
else :
#Only if the instrument is activated. Not loaded instruments are just dark black and white
if self . blackKey :
c . setNamedColor ( " #444444 " )
else :
c . setNamedColor ( " #999999 " )
self . setBrush ( c )
def hoverEnterEvent ( self , event ) :
if self . state :
l = self . parentPiano . numberLabels [ self . pitch ] . currentLabel
if l :
self . parentPiano . parentView . mainWindow . statusBar ( ) . showMessage ( f " [ { self . pitch } ] { l } " )
def hoverLeaveEvent ( self , event ) :
self . parentPiano . parentView . mainWindow . statusBar ( ) . showMessage ( " " )
class BlackKey ( QtWidgets . QGraphicsRectItem ) :
def __init__ ( self , parentPiano ) :
super ( ) . __init__ ( 0 , 0 , WIDTH , STAFFLINEGAP ) #x, y, w, h
self . parentPiano = parentPiano
self . setPen ( QtGui . QPen ( QtCore . Qt . NoPen ) )
c = QtGui . QColor ( )
c . setNamedColor ( " #444444 " )
self . setBrush ( c )
self . setOpacity ( 1.0 )
self . setEnabled ( False )
self . setAcceptedMouseButtons ( QtCore . Qt . NoButton )