#! /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 ( )