#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2019 , Nils Hilbricht , Germany ( https : / / www . hilbricht . net )
This file is part of Patroneo ( https : / / www . laborejo . org )
Laborejo 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 / > .
"""
from time import time
import engine . api as api #Session is already loaded and created, no duplication.
from template . engine import pitch
from PyQt5 import QtCore , QtGui , QtWidgets , QtOpenGL
SIZE_UNIT = 40
SIZE_TOP_OFFSET = 75
SIZE_BOTTOM_OFFSET = 35
SIZE_RIGHT_OFFSET = 80
_zValuesRelativeToScene = { #Only use for objects added directly to the scene, not children.
" off " : 1 ,
" shadow " : 4 ,
" step " : 5 ,
" scale " : 20 , #so the drop down menu is above the steps
}
class PatternGrid ( QtWidgets . QGraphicsScene ) :
"""
data example for c ' 4 d ' 8 e ' f ' 2 in a 4 / 4 timesig . Actually in any timesig .
[
{ " index:0 " , " pitch " : 60 , " factor " : 1 , " velocity " : 110 } ,
{ " index:1 " , " pitch " : 62 , " factor " : 0.5 , " velocity " : 90 } ,
{ " index:1.5 " , " pitch " : 64 , " factor " : 0.5 , " velocity " : 80 } ,
{ " index:2 " , " pitch " : 65 , " factor " : 2 , " velocity " : 60 } ,
]
We delete most of our content and redraw if the timesignature changes .
We draw all steps at once , even if hidden .
If the active track changes we only change the status ( color ) of steps but not the
steps themselves . We do not save any track state here but always react dynamically
and sent every change we do ourselves simply with the currentTrackId
"""
def __init__ ( self , parentView ) :
super ( ) . __init__ ( )
self . parentView = parentView
self . _steps = { } # (x,y):Step()
self . _labels = [ ] #Step numbers
self . _zoomFactor = 1 # no save. We don't keep a qt config.
role = QtGui . QPalette . BrightText
self . textColor = self . parentView . parentMainWindow . fPalBlue . color ( role )
self . labelColor = QtGui . QColor ( " black " ) #save for new step
self . trackName = QtWidgets . QGraphicsSimpleTextItem ( " " )
self . trackName . setBrush ( self . textColor )
self . addItem ( self . trackName )
self . trackName . setPos ( 0 , 0 )
self . scale = Scale ( parentScene = self )
self . addItem ( self . scale )
self . scale . setPos ( 0 , SIZE_TOP_OFFSET )
velocityControlsProxy = self . addWidget ( VelocityControls ( parentScene = self ) )
velocityControlsProxy . setPos ( 0 , 25 ) #we can't get the height of the track name properly. So it was trial and error...
velocityControlsProxy . setZValue ( _zValuesRelativeToScene [ " scale " ] ) #so the drop down menu is above the steps
tranposeControlsProxy = self . addWidget ( TransposeControls ( parentScene = self ) )
tranposeControlsProxy . setPos ( velocityControlsProxy . geometry ( ) . width ( ) + 10 , 25 ) #we can't get the height of the track name properly. So it was trial and error...
tranposeControlsProxy . setZValue ( _zValuesRelativeToScene [ " scale " ] ) #so the drop down menu is above the steps
self . _middleMouseDown = False
#self.ticksToPixelRatio set by callback_timeSignatureChanged
self . playhead = Playhead ( parentScene = self )
self . addItem ( self . playhead )
self . playhead . setY ( SIZE_TOP_OFFSET )
api . callbacks . timeSignatureChanged . append ( self . callback_timeSignatureChanged )
api . callbacks . patternChanged . append ( self . callback_patternChanged )
api . callbacks . trackMetaDataChanged . append ( self . callback_trackMetaDataChanged )
api . callbacks . subdivisionsChanged . append ( self . guicallback_subdivisionsChanged )
def callback_timeSignatureChanged ( self , howMany , typeInTicks ) :
""" The typeInTicks actually changes nothing visually here.
We only care about howMany steps we offer . """
self . oneMeasureInTicks = howMany * typeInTicks
self . ticksToPixelRatio = typeInTicks / SIZE_UNIT
self . _redrawSteps ( howMany )
def _redrawSteps ( self , howMany ) :
""" Draw the empty steps grid. This only happens if the pattern itself changes,
for example with the time signature or with a GUI subdivision change .
Normal step on / off is done incrementally .
"""
for existingStep in self . _steps . values ( ) :
self . removeItem ( existingStep )
self . _steps = { } # (x,y):Step()
#Build a two dimensional grid
for column in range ( howMany ) :
for row in range ( api . NUMBER_OF_STEPS ) :
x = column * SIZE_UNIT + SIZE_RIGHT_OFFSET
y = row * SIZE_UNIT + SIZE_TOP_OFFSET
step = Step ( parentScene = self , column = column , row = row )
step . setPos ( x , y )
self . addItem ( step )
self . _steps [ ( column , row ) ] = step
#there is always at least one column so we don't need to try step for AttributeError
w = step . x ( ) + SIZE_UNIT + SIZE_RIGHT_OFFSET #the position of the last step plus one step width and one offset more for good measure
h = step . y ( ) + SIZE_UNIT + SIZE_TOP_OFFSET + SIZE_BOTTOM_OFFSET #same as w
self . setSceneRect ( 0 , 0 , w , h )
def guicallback_chooseCurrentTrack ( self , exportDict ) :
""" It is guaranteed that this only happens on a real track change, not twice the same.
During the track change the currenTrackId value is still the old
track . There we introduce a force switch that enables self . guicallback_chooseCurrentTrack
to trigger a redraw even during the track change .
"""
assert not exportDict [ " id " ] == self . parentView . parentMainWindow . currentTrackId #this is still the old track.
updateMode = self . parentView . viewportUpdateMode ( ) #prevent iteration flickering from color changes.
self . parentView . setViewportUpdateMode ( self . parentView . NoViewportUpdate )
self . callback_trackMetaDataChanged ( exportDict , force = True ) #we need the color when setting pattern changed. This needs to be called before patternChanged
self . callback_patternChanged ( exportDict , force = True ) #needs to be called after trackMetaDataChanged for the color.
self . removeShadows ( )
self . parentView . setViewportUpdateMode ( updateMode )
def callback_patternChanged ( self , exportDict , force = False ) :
""" We receive the whole track as exportDict.
exportDict [ " pattern " ] is the data structure example in the class docstring .
We also receive this for every track , no matter if this our current working track .
So we check if we are the current track . However , that prevents setting up or steps
on a track change because during the track change the currenTrackId value is still the old
track . There we introduce a force switch that enables self . guicallback_chooseCurrentTrack
to trigger a redraw even during the track change .
"""
if force or exportDict [ " id " ] == self . parentView . parentMainWindow . currentTrackId :
updateMode = self . parentView . viewportUpdateMode ( ) #prevent iteration flickering from color changes.
self . parentView . setViewportUpdateMode ( self . parentView . NoViewportUpdate )
for step in self . _steps . values ( ) :
step . off ( )
for noteDict in exportDict [ " pattern " ] :
x = noteDict [ " index " ]
y = noteDict [ " pitch " ]
velocityAndFactor = ( noteDict [ " velocity " ] , noteDict [ " factor " ] )
self . _steps [ ( x , y ) ] . on ( velocityAndFactor = velocityAndFactor , exceedsPlayback = noteDict [ " exceedsPlayback " ] )
self . scale . setScale ( exportDict [ " scale " ] )
self . scale . setNoteNames ( exportDict [ " simpleNoteNames " ] )
self . parentView . setViewportUpdateMode ( updateMode )
#else ignore. We fetch new data when we change the track anyway.
#Deprectated. We do incremental updates now. But who knows if we need it in the future. I doubt it...
#def sendCurrentPatternToEngine(self):
# pattern = [step.export() for step in self._steps.values() if step.status] #engine compatible backend dict of the current GUI state. Send the switched on values.
# api.setPattern(trackId=self.parentView.parentMainWindow.currentTrackId, patternList=pattern)
def callback_trackMetaDataChanged ( self , exportDict , force = False ) :
"""
During the track change the currenTrackId value is still the old
track . There we introduce a force switch that enables self . guicallback_chooseCurrentTrack
to trigger a redraw even during the track change .
"""
if force or self . parentView . parentMainWindow . currentTrackId == exportDict [ " id " ] :
self . trackName . setText ( exportDict [ " sequencerInterface " ] [ " name " ] )
self . trackName . show ( )
c = QtGui . QColor ( exportDict [ " color " ] )
self . currentColor = c
if c . lightness ( ) > 127 : #between 0 (for black) and 255 (for white)
labelColor = QtGui . QColor ( " black " )
else :
labelColor = QtGui . QColor ( " white " )
self . labelColor = labelColor #save for new events
for step in self . _steps . values ( ) :
if step . status :
step . setBrush ( c )
step . velocityNumber . setBrush ( labelColor )
def guicallback_subdivisionsChanged ( self , newValue ) :
""" handle measuresPerGroup """
#Draw labels
for existinglabel in self . _labels :
self . removeItem ( existinglabel )
self . _labels = [ ]
for ( x , y ) , step in self . _steps . items ( ) :
step . main = not x % newValue
step . setApperance ( )
groupCounter , beatNumber = divmod ( x , newValue )
if not beatNumber :
label = QtWidgets . QGraphicsSimpleTextItem ( str ( groupCounter + 1 ) )
self . addItem ( label )
label . setBrush ( self . textColor )
x = x * SIZE_UNIT
x + = SIZE_RIGHT_OFFSET
label . setPos ( x + 3 , SIZE_TOP_OFFSET - 13 )
self . _labels . append ( label )
def showVelocities ( self ) :
for patternStep in self . _steps . values ( ) :
if patternStep . status :
patternStep . velocityNumber . show ( )
def hideVelocities ( self ) :
for patternStep in self . _steps . values ( ) :
patternStep . velocityNumber . hide ( )
def mousePressEvent ( self , event ) :
self . _middleMouseDown = False
if event . button ( ) == QtCore . Qt . MiddleButton :
self . _middleMouseDown = True
self . _lastRow = None
self . _play ( event )
event . accept ( )
if not type ( self . itemAt ( event . scenePos ( ) . x ( ) , event . scenePos ( ) . y ( ) , self . parentView . transform ( ) ) ) is Step :
self . showVelocities ( )
else :
event . ignore ( )
super ( ) . mousePressEvent ( event )
def _off ( self ) :
if not self . _lastRow is None :
api . noteOff ( self . parentView . parentMainWindow . currentTrackId , self . _lastRow )
self . _lastRow = None
def _play ( self , event ) :
assert self . _middleMouseDown
if not self . parentView . parentMainWindow . currentTrackId :
return
row = ( event . scenePos ( ) . y ( ) - SIZE_TOP_OFFSET ) / SIZE_UNIT
if row > = 0 :
row = int ( row )
else :
row = - 1
x = event . scenePos ( ) . x ( )
inside = x > SIZE_RIGHT_OFFSET and x < self . sceneRect ( ) . width ( ) - SIZE_RIGHT_OFFSET
if ( row < 0 or row > 7 ) or not inside :
row = None
if not row == self . _lastRow :
if not self . _lastRow is None :
api . noteOff ( self . parentView . parentMainWindow . currentTrackId , self . _lastRow )
if not row is None :
api . noteOn ( self . parentView . parentMainWindow . currentTrackId , row )
self . _lastRow = row
def mouseMoveEvent ( self , event ) :
""" Event button is always 0 in a mouse move event """
if self . _middleMouseDown :
event . accept ( )
self . _play ( event )
else :
#Not for us, trigger, let other items decide.
event . ignore ( )
super ( ) . mouseMoveEvent ( event )
def mouseReleaseEvent ( self , event ) :
if self . _middleMouseDown :
self . _off ( )
self . _middleMouseDown = False
if event . button ( ) == QtCore . Qt . MiddleButton :
event . accept ( )
self . _lastRow = None
self . hideVelocities ( )
else :
event . ignore ( )
super ( ) . mousePressEvent ( event )
def contextMenuEvent ( self , event ) :
menu = QtWidgets . QMenu ( )
trackId = self . parentView . parentMainWindow . currentTrackId
potentialStep = self . itemAt ( event . scenePos ( ) . x ( ) , event . scenePos ( ) . y ( ) , self . parentView . transform ( ) )
if not type ( potentialStep ) is Step :
potentialStep = None
listOfLabelsAndFunctions = [
( QtCore . QCoreApplication . translate ( " EventContextMenu " , " Invert Steps " ) , lambda : api . patternInvertSteps ( trackId ) ) ,
( QtCore . QCoreApplication . translate ( " EventContextMenu " , " All Steps On " ) , lambda : api . patternOnAllSteps ( trackId ) ) ,
( QtCore . QCoreApplication . translate ( " EventContextMenu " , " All Steps Off " ) , lambda : api . patternOffAllSteps ( trackId ) ) ,
]
if potentialStep :
listOfLabelsAndFunctions . insert ( 0 , ( QtCore . QCoreApplication . translate ( " EventContextMenu " , " Repeat to here to fill Row " ) , lambda : api . patternRowRepeatFromStep ( trackId , potentialStep . row , potentialStep . column ) ) )
listOfLabelsAndFunctions . insert ( 0 , ( QtCore . QCoreApplication . translate ( " EventContextMenu " , " Clear Row " ) , lambda : api . patternClearRow ( trackId , potentialStep . row ) ) )
listOfLabelsAndFunctions . insert ( 0 , ( QtCore . QCoreApplication . translate ( " EventContextMenu " , " Invert Row " ) , lambda : api . patternInvertRow ( trackId , potentialStep . row ) ) )
for text , function in listOfLabelsAndFunctions :
if function is None :
l = QtWidgets . QLabel ( text )
l . setAlignment ( QtCore . Qt . AlignCenter )
a = QtWidgets . QWidgetAction ( menu )
a . setDefaultWidget ( l )
menu . addAction ( a )
else :
a = QtWidgets . QAction ( text , menu )
menu . addAction ( a )
a . triggered . connect ( function )
pos = QtGui . QCursor . pos ( )
pos . setY ( pos . y ( ) + 5 )
menu . exec_ ( pos )
def wheelEvent ( self , event ) :
""" zoom, otherwise ignore event """
if QtWidgets . QApplication . keyboardModifiers ( ) == QtCore . Qt . ControlModifier :
if event . delta ( ) > 0 : #zoom in
self . _zoomFactor = min ( 5 , round ( self . _zoomFactor + 0.25 , 2 ) )
else : #zoom out
self . _zoomFactor = max ( 0.1 , round ( self . _zoomFactor - 0.25 , 2 ) )
self . _zoom ( event )
event . accept ( )
elif QtWidgets . QApplication . keyboardModifiers ( ) == QtCore . Qt . AltModifier :
potentialStep = self . itemAt ( event . scenePos ( ) . x ( ) , event . scenePos ( ) . y ( ) , self . parentView . transform ( ) )
if type ( potentialStep ) is Step :
trackId = self . parentView . parentMainWindow . currentTrackId
if event . delta ( ) > 0 :
delta = 2
else :
delta = - 2
api . patternRowChangeVelocity ( trackId , potentialStep . row , delta )
self . showVelocities ( ) #removed in self.keyReleaseEvent
event . accept ( )
else :
event . ignore ( )
super ( ) . wheelEvent ( event )
else :
event . ignore ( )
super ( ) . wheelEvent ( event )
def keyReleaseEvent ( self , event ) :
""" Complementary for wheelEvent with Alt to change row velocity.
It is hard to detect the Alt key . We just brute force because there are not many
keyPresses in Patroneo at all . """
self . hideVelocities ( )
event . ignore ( )
super ( ) . keyReleaseEvent ( event )
def _zoom ( self , event ) :
if 0.1 < self . _zoomFactor < 5 :
self . parentView . resetTransform ( )
self . parentView . scale ( self . _zoomFactor , self . _zoomFactor )
self . parentView . centerOn ( event . scenePos ( ) )
def createShadow ( self , exportDict ) :
""" Receives steps from another track and display them as shadoy steps in the current one
as a reference . Creating a new shadow does not delete the old one .
"""
for x , y in ( ( s [ " index " ] , s [ " pitch " ] ) for s in exportDict [ " pattern " ] ) :
self . _steps [ ( x , y ) ] . shadow = True # (x,y):Step()
self . _steps [ ( x , y ) ] . setApperance ( )
def removeShadows ( self ) :
for step in self . _steps . values ( ) :
if step . shadow :
step . shadow = False
step . setApperance ( )
class Step ( QtWidgets . QGraphicsRectItem ) :
""" The representation of a note """
def __init__ ( self , parentScene , column , row ) : #Factor and Velocity are set on activation
self . parentScene = parentScene
self . column = column #grid coordinates, not pixels
self . row = row
offset = 2
self . offset = offset
self . defaultSize = ( offset , offset , SIZE_UNIT - offset * 2 , SIZE_UNIT - offset * 2 ) #x, y, w, h
super ( ) . __init__ ( * self . defaultSize )
self . setAcceptHoverEvents ( True )
self . setFlags ( QtWidgets . QGraphicsItem . ItemIsFocusable ) #to receive key press events
self . main = True
self . exceedsPlayback = False
self . factor = api . DEFAULT_FACTOR
self . status = False
self . _factorChangeAllowed = False #during drag and drop this will be True. Used in the mouse steps.
self . shadow = False
#Velocity
self . _rememberVelocity = None
self . velocityNumber = QtWidgets . QGraphicsSimpleTextItem ( )
self . velocityNumber . setParentItem ( self )
self . velocityNumber . setBrush ( self . parentScene . labelColor )
self . velocityNumber . setPos ( offset * 2 , offset * 2 ) #that is not pretty but you can see it under the cursor
self . velocityNumber . hide ( ) #only visible during mouse wheel event
#The data section. On creation all the steps are uninitialized. They are off and hold no musical values
#self.velocity = set in self.useDefaultValues . We don't set it at all here to have a clear Exception when the program tries to access it.
#self.factor = set in self.useDefaultValues . We don't set it at all here to have a clear Exception when the program tries to access it.
#self.pitch #this is determined by the position on the grid
self . setApperance ( ) #sets color, size and exceedPlayback warning. not velocity.
def setApperance ( self ) :
""" sets color, main/sub size and exceedPlayback warning. not velocity.
This gets called quite often . On mouse down and on release for starters . """
def setWidth ( ) :
if not self . exceedsPlayback and self . x ( ) + self . rect ( ) . width ( ) + SIZE_RIGHT_OFFSET > self . parentScene . sceneRect ( ) . right ( ) :
self . exceedsPlayback = True
if self . exceedsPlayback :
rect = self . rect ( )
maximumWidth = self . parentScene . sceneRect ( ) . right ( ) - self . x ( ) - SIZE_RIGHT_OFFSET - self . offset * 2
rect . setWidth ( maximumWidth )
self . setRect ( rect )
else :
rect = self . rect ( )
rect . setWidth ( SIZE_UNIT * self . factor - self . offset * 2 )
self . setRect ( rect )
if self . status :
setWidth ( )
assert self . parentScene . currentColor
self . setBrush ( self . parentScene . currentColor )
self . velocityNumber . setBrush ( self . parentScene . labelColor )
self . setZValue ( _zValuesRelativeToScene [ " step " ] )
else :
self . setOpacity ( 1 )
if self . shadow :
setWidth ( )
color = self . parentScene . parentView . parentMainWindow . fPalBlue . color ( QtGui . QPalette . Shadow ) #this is already an existing instance
self . setOpacity ( 0.3 )
self . setZValue ( _zValuesRelativeToScene [ " shadow " ] )
elif self . main :
color = self . parentScene . parentView . parentMainWindow . fPalBlue . color ( QtGui . QPalette . AlternateBase ) #this is already an existing instance
self . setZValue ( _zValuesRelativeToScene [ " off " ] )
else :
color = self . parentScene . parentView . parentMainWindow . fPalBlue . color ( QtGui . QPalette . Base ) #this is already an existing instance
self . setZValue ( _zValuesRelativeToScene [ " off " ] )
self . setBrush ( color )
@property
def velocity ( self ) :
return self . _velocity
@velocity . setter
def velocity ( self , value ) :
self . _velocity = value
self . velocityNumber . setText ( str ( value ) )
self . setOpacity ( self . _compress ( value , 1 , 127 , 0.4 , 1.0 ) )
def _compress ( self , input , inputLowest , inputHighest , outputLowest , outputHighest ) :
return ( input - inputLowest ) / ( inputHighest - inputLowest ) * ( outputHighest - outputLowest ) + outputLowest
def export ( self ) :
""" Make a dict to send to the engine """
return {
" index " : self . column ,
" pitch " : self . row ,
" factor " : self . factor ,
" velocity " : self . velocity }
def useDefaultValues ( self ) :
self . velocity = api . getAverageVelocity ( trackId = self . parentScene . parentView . parentMainWindow . currentTrackId ) #already sets opacity and velocityNumber
self . _rememberVelocity = self . velocity
self . factor = api . DEFAULT_FACTOR
self . initalized = True
def on ( self , velocityAndFactor = None , exceedsPlayback = None ) :
""" velocityAndFactor is a tuple """
if velocityAndFactor : #on load / by callback
self . velocity , self . factor = velocityAndFactor
else : #User clicked on an empty field.
self . useDefaultValues ( )
self . exceedsPlayback = exceedsPlayback
assert self . factor > 0
rect = self . rect ( )
#rect.setWidth(self.defaultSize[2] * self.factor)
rect . setWidth ( SIZE_UNIT * self . factor - self . offset * 2 )
self . setRect ( rect )
self . status = True
self . setApperance ( ) #sets color, main/sub size and exceedPlayback warning
def off ( self ) :
self . status = False
self . setRect ( * self . defaultSize )
self . setApperance ( ) #sets color, main/sub size and exceedPlayback warning
self . velocityNumber . hide ( ) #just in case.
def mousePressEvent ( self , event ) :
if event . button ( ) == QtCore . Qt . LeftButton :
event . accept ( )
if self . status :
self . off ( )
api . removeStep ( self . parentScene . parentView . parentMainWindow . currentTrackId , self . column , self . row )
else :
self . on ( )
self . _factorChangeAllowed = True
self . _factorStartTime = time ( ) #see mouseReleaseEvent
else :
event . ignore ( )
def mouseMoveEvent ( self , event ) :
if self . _factorChangeAllowed :
# < is left to right
# > is right to left
event . accept ( )
rect = self . rect ( )
if event . lastScenePos ( ) . x ( ) < event . scenePos ( ) . x ( ) :
new = event . scenePos ( ) . x ( ) - self . x ( )
else :
new = max ( self . defaultSize [ 2 ] / 2 , event . scenePos ( ) . x ( ) - self . x ( ) ) #pixel values, not tick, nor factor
rect . setRight ( new )
self . setRect ( rect )
def mouseReleaseEvent ( self , event ) :
if self . _factorChangeAllowed :
assert self . status
self . _factorChangeAllowed = False
width = self . rect ( ) . width ( ) + self . offset * 2
value = width / SIZE_UNIT
elapsedTime = time ( ) - self . _factorStartTime #to prevent hectic mouse pressing from triggering the factor we only accept a change if a certain time treshold was passed
if ( elapsedTime > 0.2 and value > = 0.5 ) : # or value == 0.5:
self . factor = value
self . setApperance ( ) #sets color, size and exceedPlayback warning
else : # A quick mouseclick
assert self . factor == 1
self . setRect ( * self . defaultSize ) #we reset this in case something goes wrong. If everything is all right we will a receive a callback to set the width anyway, before the user sees anything.
self . setApperance ( ) #sets color, size and exceedPlayback warning
api . setStep ( self . parentScene . parentView . parentMainWindow . currentTrackId , self . export ( ) )
event . accept ( )
def hoverEnterEvent ( self , event ) :
""" Hover events are only triggered if no mouse button is down. The mouse got grabbed
by the item . Which makes sense because we want to receive mouseRelease on an item even
if the mouse cursor is not on that item anymore """
if self . status :
event . accept ( )
self . _rememberVelocity = self . velocity
else :
event . ignore ( )
def hoverLeaveEvent ( self , event ) :
""" Hover events are only triggered if no mouse button is down. The mouse got grabbed
by the item . Which makes sense because we want to receive mouseRelease on an item even
if the mouse cursor is not on that item anymore """
self . velocityNumber . hide ( )
if self . status :
event . accept ( )
if self . status and not self . velocity == self . _rememberVelocity :
api . setStep ( self . parentScene . parentView . parentMainWindow . currentTrackId , self . export ( ) )
self . _rememberVelocity = self . velocity
else :
event . ignore ( )
def wheelEvent ( self , event ) :
""" This buffers until hoverLeaveEvent and then the new value is sent in self.hoverLeaveEvent """
if self . status :
event . accept ( )
self . velocityNumber . show ( )
if event . delta ( ) > 0 :
self . velocity + = 2
if self . velocity > = 127 :
self . velocity = 127
else :
self . velocity - = 2
if self . velocity < = 2 :
self . velocity = 1
else :
event . ignore ( )
class Scale ( QtWidgets . QGraphicsRectItem ) :
def __init__ ( self , parentScene ) :
super ( ) . __init__ ( 0 , 0 , 0 , 0 )
self . parentScene = parentScene
self . pitchWidgets = [ ] #sorted from top to bottom in Step Rect and scene coordinates
self . simpleNoteNames = [ ] #list of 128 notes. use index with note name. Can be changed at runtime.
api . callbacks . trackMetaDataChanged . append ( self . callback_trackMetaDataChanged )
self . buildScale ( ) #also sets the positions of the buttons above
def callback_trackMetaDataChanged ( self , exportDict ) :
#Order matters. We need to set the notenames before the scale.
self . setNoteNames ( exportDict [ " simpleNoteNames " ] )
self . setScale ( exportDict [ " scale " ] )
def buildScale ( self ) :
""" Only executed once per pattern """
for i in range ( api . NUMBER_OF_STEPS ) :
p = PitchWidget ( parentItem = self )
y = i * SIZE_UNIT
p . setParentItem ( self )
p . setPos ( - 65 , y + 10 )
self . pitchWidgets . append ( p )
#self.setRect(0,0, SIZE_RIGHT_OFFSET, p.y() + SIZE_UNIT) #p is the last of the 8.
def setScale ( self , scaleList ) :
""" We receive from top to bottom, in step rect coordinates. This is not sorted after
pitches ! Pitches can be any order the user wants .
"""
for widget , scaleMidiPitch in zip ( self . pitchWidgets , scaleList ) :
widget . spinBox . setValue ( scaleMidiPitch )
widget . rememberLastValue = scaleMidiPitch
def setNoteNames ( self , pNoteNames ) :
""" A list of 128 strings. Gets only called by the callback.
E . g . it happens when you switch the active gui track """
#if pNoteNames in pitch.notenames.keys():
# self.simpleNoteNames = pitch.notenames[pNoteNames]
#else:
self . simpleNoteNames = pNoteNames
for pitchWidget in self . pitchWidgets :
pitchWidget . spinBoxValueChanged ( ) #change all current pitchWidgets
def sendToEngine ( self , callback = True ) :
result = [ widget . spinBox . value ( ) for widget in self . pitchWidgets ]
#result.reverse()
trackId = self . parentScene . parentView . parentMainWindow . currentTrackId
if trackId : #startup check
api . setScale ( trackId , scale = result , callback = callback )
class TransposeControls ( QtWidgets . QWidget ) :
""" Communication with the scale spinBoxes is done via api callbacks. We just fire and forget """
#Not working. the translation generate works statically. translatedScales = [QtCore.QT_TRANSLATE_NOOP("Scale", scale) for scale in api.schemes]
#No choice but to prepare the translations manually here. At least we do not need to watch for the order.
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Major " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Minor " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Dorian " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Phrygian " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Lydian " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Mixolydian " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Locrian " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Blues " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Hollywood " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Chromatic " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " English " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Lilypond " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " German " )
QtCore . QT_TRANSLATE_NOOP ( " Scale " , " Drums GM " )
def __init__ ( self , parentScene ) :
self . parentScene = parentScene
super ( ) . __init__ ( )
layout = QtWidgets . QHBoxLayout ( )
layout . setSpacing ( 0 )
layout . setContentsMargins ( 0 , 0 , 0 , 0 )
self . setLayout ( layout )
transposeUp = QtWidgets . QPushButton ( QtCore . QCoreApplication . translate ( " TransposeControls " , " +Half Tone " ) )
transposeUp . clicked . connect ( self . transposeUp )
transposeUp . setToolTip ( QtCore . QCoreApplication . translate ( " TransposeControls " , " Transpose the whole scale up a half tone (+1 midi note) " ) )
layout . addWidget ( transposeUp )
transposeDown = QtWidgets . QPushButton ( QtCore . QCoreApplication . translate ( " TransposeControls " , " -Half Tone " ) )
transposeDown . clicked . connect ( self . transposeDown )
transposeDown . setToolTip ( QtCore . QCoreApplication . translate ( " TransposeControls " , " Transpose the whole scale down a half tone (-1 midi note) " ) )
layout . addWidget ( transposeDown )
transposeUpOctave = QtWidgets . QPushButton ( QtCore . QCoreApplication . translate ( " TransposeControls " , " +Octave " ) )
transposeUpOctave . clicked . connect ( self . transposeUpOctave )
transposeUpOctave . setToolTip ( QtCore . QCoreApplication . translate ( " TransposeControls " , " Transpose the whole scale up an octave (+12 midi notes) " ) )
layout . addWidget ( transposeUpOctave )
transposeDownOctave = QtWidgets . QPushButton ( QtCore . QCoreApplication . translate ( " TransposeControls " , " -Octave " ) )
transposeDownOctave . clicked . connect ( self . transposeDownOctave )
transposeDownOctave . setToolTip ( QtCore . QCoreApplication . translate ( " TransposeControls " , " Transpose the whole scale down an octave (-12 midi notes) " ) )
layout . addWidget ( transposeDownOctave )
translatedSchemes = [ QtCore . QCoreApplication . translate ( " Scale " , scheme ) for scheme in api . schemes ]
transposeToScale = QtWidgets . QComboBox ( )
self . _transposeToScaleWidget = transposeToScale
transposeToScale . addItems ( [ QtCore . QCoreApplication . translate ( " TransposeControls " , " Set Scale to: " ) ] + translatedSchemes ) #This is a hack. QProxyWidgets will draw outside of the view and cannot be seen anymore. We reset to the 0th entry after each change.
transposeToScale . activated . connect ( self . transposeToScale ) #activated, not changend. even when choosing the same item
transposeToScale . setToolTip ( QtCore . QCoreApplication . translate ( " TransposeControls " , " Take the bottom note and build a predefined scale from it upwards. " ) )
layout . addWidget ( transposeToScale )
self . _comboBoxNoteNames = QtWidgets . QComboBox ( )
translatedNotenames = [ QtCore . QCoreApplication . translate ( " Scale " , scheme ) for scheme in sorted ( list ( pitch . simpleNoteNames . keys ( ) ) ) ]
self . _comboBoxNoteNames . addItems ( [ QtCore . QCoreApplication . translate ( " TransposeControls " , " Set Notenames to: " ) ] + translatedNotenames )
self . _comboBoxNoteNames . activated . connect ( self . _changeNoteNamesByDropdown ) #activated, not changend. even when choosing the same item
self . _comboBoxNoteNames . setToolTip ( QtCore . QCoreApplication . translate ( " TransposeControls " , " Use this scheme as note names. " ) )
layout . addWidget ( self . _comboBoxNoteNames )
def _changeNoteNamesByDropdown ( self , index ) :
if index > 0 :
index - = 1 # we inserted our "Set NoteNames to" at index 0 shifting the real values +1.
schemes = sorted ( pitch . simpleNoteNames . keys ( ) )
noteNamesAsString = sorted ( pitch . simpleNoteNames . keys ( ) ) [ index ]
simpleNoteNames = pitch . simpleNoteNames [ noteNamesAsString ]
api . setSimpleNoteNames ( trackId = self . parentScene . parentView . parentMainWindow . currentTrackId , simpleNoteNames = simpleNoteNames )
self . _comboBoxNoteNames . blockSignals ( True )
self . _comboBoxNoteNames . setCurrentIndex ( 0 )
self . _comboBoxNoteNames . blockSignals ( False )
def transposeUp ( self ) :
api . transposeHalftoneSteps ( trackId = self . parentScene . parentView . parentMainWindow . currentTrackId , steps = 1 )
def transposeDown ( self ) :
api . transposeHalftoneSteps ( trackId = self . parentScene . parentView . parentMainWindow . currentTrackId , steps = - 1 )
def transposeUpOctave ( self ) :
api . transposeHalftoneSteps ( trackId = self . parentScene . parentView . parentMainWindow . currentTrackId , steps = 12 )
def transposeDownOctave ( self ) :
api . transposeHalftoneSteps ( trackId = self . parentScene . parentView . parentMainWindow . currentTrackId , steps = - 12 )
def transposeToScale ( self , index ) :
if index > 0 :
index - = 1 # the backend list obviously has no "Set Scale to" on index [0]
api . setScaleToKeyword ( trackId = self . parentScene . parentView . parentMainWindow . currentTrackId , keyword = api . schemes [ index ] ) #this schemes must NOT be translated since it is the original key/symbol.
self . _transposeToScaleWidget . blockSignals ( True )
self . _transposeToScaleWidget . setCurrentIndex ( 0 )
self . _transposeToScaleWidget . blockSignals ( False )
class PitchWidget ( QtWidgets . QGraphicsProxyWidget ) :
""" A PitchWidget has a variable width by nature because the note-name can vary.
For that reason We need to truncate to match the fixed size .
Offset and position are set in Scale . buildScale
"""
def __init__ ( self , parentItem ) :
super ( ) . __init__ ( )
self . parentItem = parentItem
self . spinBox = QtWidgets . QSpinBox ( )
#self.spinBox.setFrame(True)
self . spinBox . setMinimum ( 0 )
self . spinBox . setMaximum ( 127 )
self . spinBox . stepBy = self . stepBy
#self.spinBox.setValue(0) #No init value. This is changed on active track callback
widget = QtWidgets . QWidget ( )
layout = QtWidgets . QHBoxLayout ( )
layout . setSpacing ( 0 )
layout . setContentsMargins ( 0 , 0 , 0 , 0 )
widget . setLayout ( layout )
widget . setStyleSheet ( " .QWidget { background-color: rgba(0,0,0,0) } " ) #transparent, but only this widget, hence the leading dot
self . label = QtWidgets . QLabel ( ) #changed in spinBoxValueChanged
self . label . setText ( " " )
self . label . setFixedSize ( 90 , 18 )
self . label . setAlignment ( QtCore . Qt . AlignRight | QtCore . Qt . AlignVCenter )
layout . addWidget ( self . label )
layout . addWidget ( self . spinBox )
self . setWidget ( widget )
self . spinBox . wheelEvent = self . spinBoxMouseWheelEvent
self . spinBox . valueChanged . connect ( self . spinBoxValueChanged )
self . spinBox . editingFinished . connect ( self . spinBoxEditingFinished )
#self.spinBoxValueChanged() #Delay that. The engine Data is not ready yet. It will be sent by the callback
arrowsLeftStylesheet = """
QSpinBox {
padding - left : 15 px ; / * make room for the arrows * /
}
QSpinBox : : up - button {
subcontrol - position : top left ; / * position at the top right corner * /
}
QSpinBox : : down - button {
subcontrol - position : bottom left ; / * position at bottom right corner * /
}
"""
#That does not looks good
#self.spinBox.setStyleSheet(arrowsLeftStylesheet)
def midiToNotename ( self , midipitch ) :
assert self . parentItem . simpleNoteNames , self . parentItem . simpleNoteNames
try :
return self . parentItem . simpleNoteNames [ midipitch ] #includes octave names
except IndexError :
print ( midipitch )
print ( self . parentItem . simpleNoteNames )
exit ( )
def spinBoxValueChanged ( self ) :
self . label . setText ( self . midiToNotename ( self . spinBox . value ( ) ) )
#self.parentItem.sendToEngine(callback=False) # results in a loop with callback, and in wrong data without. This is not the right place to implement immediate note feedback while editing is still going on.
def spinBoxEditingFinished ( self ) :
if not self . rememberLastValue == self . spinBox . value ( ) :
self . parentItem . sendToEngine ( )
self . rememberLastValue = self . spinBox . value ( )
def stepBy ( self , n ) :
""" Override standard behaviour to make page up and page down go in octaves, not in 10 """
if n == 10 :
QtWidgets . QAbstractSpinBox . stepBy ( self . spinBox , 12 )
elif n == - 10 :
QtWidgets . QAbstractSpinBox . stepBy ( self . spinBox , - 12 )
else :
QtWidgets . QAbstractSpinBox . stepBy ( self . spinBox , n )
def spinBoxMouseWheelEvent ( self , event ) :
""" We cannot use spinBoxValueChanged to send mousewheel scrolling pitch changing directly
to the engine while editing is still active . this results in signal loops and various
data corruptions . Fixing this would be far too much work .
You can either use the arrow keys and press enter , which triggers editingFinished .
But here we intercept the mousewheel directly . """
event . ignore ( )
QtWidgets . QSpinBox . wheelEvent ( self . spinBox , event ) #this changes to the new text and therefore the new value. Call BEFORE sendToEngine
self . parentItem . sendToEngine ( callback = False )
#if event.angleDelta().y() > 0: #up
#else: #down
class Playhead ( QtWidgets . QGraphicsLineItem ) :
def __init__ ( self , parentScene ) :
super ( ) . __init__ ( 0 , 0 , 0 , api . NUMBER_OF_STEPS * SIZE_UNIT ) # (x1, y1, x2, y2)
self . parentScene = parentScene
p = QtGui . QPen ( )
p . setColor ( QtGui . QColor ( " red " ) )
p . setWidth ( 3 )
#p.setCosmetic(True)
self . setPen ( p )
api . callbacks . setPlaybackTicks . append ( self . setCursorPosition )
self . setZValue ( 90 )
def setCursorPosition ( self , tickindex , playbackStatus ) :
""" Using modulo makes the playback cursor wrap around and play over the pattern
eventhough we use the global tick value . """
x = ( tickindex % self . parentScene . oneMeasureInTicks ) / self . parentScene . ticksToPixelRatio
x + = SIZE_RIGHT_OFFSET
if playbackStatus : # api.duringPlayback:
self . show ( )
self . setX ( x )
scenePos = self . parentScene . parentView . mapFromScene ( self . pos ( ) )
cursorViewPosX = scenePos . x ( ) #the cursor position in View coordinates
width = self . parentScene . parentView . geometry ( ) . width ( )
if cursorViewPosX < = 0 or cursorViewPosX > = width : #"pageflip"
self . parentScene . parentView . horizontalScrollBar ( ) . setValue ( x )
else :
self . hide ( )
class VelocityControls ( QtWidgets . QWidget ) :
def __init__ ( self , parentScene ) :
self . parentScene = parentScene
super ( ) . __init__ ( )
layout = QtWidgets . QHBoxLayout ( )
layout . setSpacing ( 0 )
layout . setContentsMargins ( 0 , 0 , 0 , 0 )
self . setLayout ( layout )
velocityUp = QtWidgets . QPushButton ( QtCore . QCoreApplication . translate ( " VelocityControls " , " +Velocity " ) )
velocityUp . clicked . connect ( self . velocityUp )
velocityUp . wheelEvent = self . _mouseWheelEvent
velocityUp . setToolTip ( QtCore . QCoreApplication . translate ( " VelocityControls " , " Make everything louder. Hover and mousewheel up/down to go in steps of 10. " ) )
layout . addWidget ( velocityUp )
velocityDown = QtWidgets . QPushButton ( QtCore . QCoreApplication . translate ( " VelocityControls " , " -Velocity " ) )
velocityDown . clicked . connect ( self . velocityDown )
velocityDown . wheelEvent = self . _mouseWheelEvent
velocityDown . setToolTip ( QtCore . QCoreApplication . translate ( " VelocityControls " , " Make everything softer. Hover and mousewheel up/down to go in steps of 10. " ) )
layout . addWidget ( velocityDown )
def _mouseWheelEvent ( self , event ) :
event . accept ( )
if event . angleDelta ( ) . y ( ) > 0 : #up
api . changePatternVelocity ( trackId = self . parentScene . parentView . parentMainWindow . currentTrackId , steps = 10 )
else : #down
api . changePatternVelocity ( trackId = self . parentScene . parentView . parentMainWindow . currentTrackId , steps = - 10 )
#The steps got redrawn, therefore their velocity numbers are now invislbe again. We are still in hover modus:
self . parentScene . showVelocities ( )
def enterEvent ( self , event ) :
self . parentScene . showVelocities ( )
def leaveEvent ( self , event ) :
self . parentScene . hideVelocities ( )
def velocityUp ( self ) :
api . changePatternVelocity ( trackId = self . parentScene . parentView . parentMainWindow . currentTrackId , steps = 1 )
#The steps got redrawn, therefore their velocity numbers are now invislbe again. We are still in hover modus:
self . parentScene . showVelocities ( )
def velocityDown ( self ) :
api . changePatternVelocity ( trackId = self . parentScene . parentView . parentMainWindow . currentTrackId , steps = - 1 )
#The steps got redrawn, therefore their velocity numbers are now invislbe again. We are still in hover modus:
self . parentScene . showVelocities ( )