#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2022 , 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 / > .
"""
import logging ; logger = logging . getLogger ( __name__ ) ; logger . info ( " import " )
#Standard Library
from fractions import Fraction
from time import time
#Third Party
from PyQt5 import QtCore , QtGui , QtWidgets
#Template
import template . qtgui . helper as helper
#Our modules
import engine . api as api #Session is already loaded and created, no duplication.
SIZE_UNIT = 30 #this is in manual sync with timeline.py SIZE_UNIT
SIZE_TOP_OFFSET = 0
_zValuesRelativeToScene = { #Only use for objects added directly to the scene, not children.
" trackStructure " : 3 ,
" barline " : 5 ,
" group " : 6 ,
" switch " : 7 ,
" barlineGroupHighlight " : 9 ,
" playhead " : 90 ,
}
class SongEditor ( QtWidgets . QGraphicsScene ) :
def __init__ ( self , parentView ) :
super ( ) . __init__ ( )
self . parentView = parentView
self . statusMessage = self . parentView . parentMainWindow . statusBar ( ) . showMessage #a version with the correct path of this is in every class of Patroneo
#Set color, otherwise it will be transparent in window managers or wayland that want that.
self . backColor = QtGui . QColor ( 55 , 61 , 69 )
self . setBackgroundBrush ( self . backColor )
self . _exportDictScore = None #cached
#Subitems
self . playhead = Playhead ( parentScene = self )
self . addItem ( self . playhead )
self . playhead . setY ( SIZE_TOP_OFFSET )
self . _groupRectangles = [ ]
self . tracks = { } #TrackID:TrackStructures
self . barlines = [ ] #in order
self . cachedCombinedTrackHeight = 0 #set by callback_tracksChanged
self . trackOrder = [ ] #contains engine-ids, set by callback_numberOfTracksChanged
role = QtGui . QPalette . BrightText
self . brightPen = QtGui . QPen ( self . parentView . parentMainWindow . fPalBlue . color ( role ) )
self . normalPen = QtGui . QPen ( )
self . currentHoverStep = None #either a Step() object or None. Only set when hovering active steps.
api . callbacks . numberOfTracksChanged . append ( self . callback_numberOfTracksChanged )
api . callbacks . timeSignatureChanged . append ( self . callback_timeSignatureChanged )
api . callbacks . numberOfMeasuresChanged . append ( self . callback_setnumberOfMeasures )
api . callbacks . trackStructureChanged . append ( self . callback_trackStructureChanged ) #updates single tracks
api . callbacks . exportCacheChanged . append ( self . cacheExportDict )
api . callbacks . scoreChanged . append ( self . callback_scoreChanged ) #sends information about measuresPerGroup
api . callbacks . trackMetaDataChanged . append ( self . callback_trackMetaDataChanged )
api . callbacks . patternLengthMultiplicatorChanged . append ( self . callback_patternLengthMultiplicatorChanged )
#self.ticksToPixelRatio = None set by callback_timeSignatureChanged
def wheelEvent ( self , event ) :
""" zoom, otherwise ignore event """
if QtWidgets . QApplication . keyboardModifiers ( ) == QtCore . Qt . ControlModifier :
self . parentView . parentMainWindow . zoomUpperHalf ( event )
event . accept ( )
else :
event . ignore ( )
super ( ) . wheelEvent ( event )
def callback_trackMetaDataChanged ( self , exportDict ) :
""" This is not for the initial track creation, only for later changes """
self . tracks [ exportDict [ " id " ] ] . updateMetaData ( exportDict )
def cacheExportDict ( self , exportDict ) :
""" Does not get called on structure change because callback_trackStructureChanged
also caches the exportDict """
self . tracks [ exportDict [ " id " ] ] . exportDict = exportDict
def callback_trackStructureChanged ( self , exportDict ) :
""" Happens if a switch gets flipped """
track = self . tracks [ exportDict [ " id " ] ]
track . updateSwitches ( exportDict )
def callback_timeSignatureChanged ( self , nr , typ ) :
oneMeasureInTicks = nr * typ
self . ticksToPixelRatio = oneMeasureInTicks / SIZE_UNIT
def callback_numberOfTracksChanged ( self , exportDictList ) :
""" Used for new tracks, delete track and move track. Also groups and visibility. """
toDelete = set ( self . tracks . keys ( ) )
self . trackOrder = [ ]
groupsSeen = set ( ) #check if we already know this group
for grect in self . _groupRectangles :
self . removeItem ( grect ) #group rectangles are direct children of the scene. delete them here.
self . _groupRectangles = [ ]
groupOffset = 0 #pixels. It is a positive/absolute value
hiddenOffsetCounter = 0 #counts hidden tracks in pixels. It is a positive/absolute value
for index , exportDict in enumerate ( exportDictList ) :
if exportDict [ " id " ] in self . tracks :
toDelete . remove ( exportDict [ " id " ] ) #keep this track and don't delete later.
else : #new track
self . tracks [ exportDict [ " id " ] ] = TrackStructure ( parentScene = self )
self . addItem ( self . tracks [ exportDict [ " id " ] ] )
self . tracks [ exportDict [ " id " ] ] . setZValue ( _zValuesRelativeToScene [ " trackStructure " ] )
if exportDict [ " group " ] :
if not exportDict [ " group " ] in groupsSeen : #first encounter
groupsSeen . add ( exportDict [ " group " ] )
groupRect = QtWidgets . QGraphicsRectItem ( 0 , 0 , exportDict [ " numberOfMeasures " ] * SIZE_UNIT , SIZE_UNIT )
groupRect . trackGroup = exportDict [ " group " ] #add a marker for double clicks, so that we don't have to create a whole new class.
role = QtGui . QPalette . Window
c = self . parentView . parentMainWindow . fPalBlue . color ( role )
groupRect . setBrush ( c )
self . addItem ( groupRect )
groupRect . setY ( index * SIZE_UNIT + SIZE_TOP_OFFSET + groupOffset - hiddenOffsetCounter )
groupRect . setZValue ( _zValuesRelativeToScene [ " group " ] )
self . _groupRectangles . append ( groupRect )
groupOffset = len ( groupsSeen ) * SIZE_UNIT
if exportDict [ " visible " ] :
self . tracks [ exportDict [ " id " ] ] . show ( )
else :
self . tracks [ exportDict [ " id " ] ] . hide ( )
hiddenOffsetCounter + = SIZE_UNIT
self . trackOrder . append ( self . tracks [ exportDict [ " id " ] ] )
self . tracks [ exportDict [ " id " ] ] . setY ( index * SIZE_UNIT + SIZE_TOP_OFFSET + groupOffset - hiddenOffsetCounter )
self . tracks [ exportDict [ " id " ] ] . updateSwitches ( exportDict )
self . tracks [ exportDict [ " id " ] ] . updateStaffLines ( exportDict [ " numberOfMeasures " ] )
#We had these tracks in the GUI but they are gone in the export. This is track delete.
for trackId in toDelete :
trackStructure = self . tracks [ trackId ]
for position , switch in trackStructure . switches . items ( ) :
self . removeItem ( switch ) #switches are direct children of the scene. delete them here.
#we don't need to delete from trackOrder here because that is cleared each time we call this function
del self . tracks [ trackId ]
self . removeItem ( trackStructure ) #remove from scene
del trackStructure
assert all ( track . exportDict [ " sequencerInterface " ] [ " index " ] == self . trackOrder . index ( track ) for track in self . tracks . values ( ) )
self . cachedCombinedTrackHeight = len ( self . tracks ) * SIZE_UNIT + groupOffset - hiddenOffsetCounter
self . setSceneRect ( 0 , 0 , exportDict [ " numberOfMeasures " ] * SIZE_UNIT , self . cachedCombinedTrackHeight + 3 * SIZE_TOP_OFFSET ) #no empty space on top without a scene rect. Also a bit of leniance.
self . playhead . setLine ( 0 , 0 , 0 , self . cachedCombinedTrackHeight ) #(x1, y1, x2, y2)
self . adjustBarlineHeightForNewTrackCount ( )
def adjustBarlineHeightForNewTrackCount ( self ) :
""" Fetches the current context itself and modifies all existing barlines.
"""
for barline in self . barlines :
barline . setLine ( 0 , 0 , 0 , self . cachedCombinedTrackHeight )
def callback_setnumberOfMeasures ( self , exportDictScore ) :
self . _exportDictScore = exportDictScore
requestAmountOfMeasures = exportDictScore [ " numberOfMeasures " ]
requestAmountOfMeasures + = 1 #the final closing barline
maximumAmountIncludingHidden = len ( self . barlines )
if requestAmountOfMeasures == maximumAmountIncludingHidden :
for l in self . barlines : l . show ( )
elif requestAmountOfMeasures > maximumAmountIncludingHidden : #we need more than we have. Maybe new ones.
for l in self . barlines : l . show ( )
for i in range ( maximumAmountIncludingHidden , requestAmountOfMeasures ) :
barline = QtWidgets . QGraphicsLineItem ( 0 , 0 , 0 , 1 ) #correct length will be set below, but we need something other than 0 here
self . addItem ( barline )
barline . setAcceptedMouseButtons ( QtCore . Qt . NoButton ) #barlines will intercept clicks on the track otherwise. We keep the horizontal stafflines blocking to prevent accidents though.
barline . setPos ( i * SIZE_UNIT , SIZE_TOP_OFFSET )
barline . setEnabled ( False )
barline . setZValue ( _zValuesRelativeToScene [ " barline " ] )
self . barlines . append ( barline )
else : #user reduced the number of barlines. We only hide, never delete.
for l in self . barlines [ requestAmountOfMeasures : ] :
l . hide ( )
#Adjust Group Header Rectangles
for groupRect in self . _groupRectangles :
r = groupRect . rect ( )
r . setWidth ( ( requestAmountOfMeasures - 1 ) * SIZE_UNIT )
groupRect . setRect ( r )
#Guaranteed visible.
for l in self . barlines [ : requestAmountOfMeasures ] :
l . show ( )
self . callback_scoreChanged ( exportDictScore ) #colors from the start
self . adjustBarlineHeightForNewTrackCount ( ) #otherwise only the new ones have the correct height.
self . setSceneRect ( 0 , 0 , requestAmountOfMeasures * SIZE_UNIT , self . cachedCombinedTrackHeight + 3 * SIZE_TOP_OFFSET ) #no empty space on top without a scene rect
for track in self . tracks . values ( ) :
factor = track . exportDict [ " patternLengthMultiplicator " ]
track . updateSwitchVisibility ( requestAmountOfMeasures = ( requestAmountOfMeasures - 1 ) / / factor )
track . updateStaffLines ( requestAmountOfMeasures - 1 )
def callback_scoreChanged ( self , exportDictScore ) :
self . _exportDictScore = exportDictScore
self . measuresPerGroupCache = exportDictScore [ " measuresPerGroup " ]
for i , barline in enumerate ( self . barlines ) :
if i > 0 and ( i + 1 ) % exportDictScore [ " measuresPerGroup " ] == 1 :
barline . setPen ( self . brightPen )
barline . setZValue ( _zValuesRelativeToScene [ " barlineGroupHighlight " ] )
else :
barline . setPen ( self . normalPen )
barline . setZValue ( _zValuesRelativeToScene [ " barline " ] )
def callback_patternLengthMultiplicatorChanged ( self , exportDict ) :
""" This is only for a single track. We relay it """
track = self . tracks [ exportDict [ " id " ] ]
track . updatePatternLengthMultiplicator ( exportDict )
def mouseDoubleClickEvent ( self , event ) :
item = self . itemAt ( event . scenePos ( ) . x ( ) , event . scenePos ( ) . y ( ) , self . parentView . transform ( ) )
event . ignore ( ) #send to child widget
if item :
try :
item . trackGroup
api . setGroupVisible ( item . trackGroup )
except :
pass
class TrackStructure ( QtWidgets . QGraphicsRectItem ) :
""" From left to right. Holds two lines to show the " staffline " and a number of switches,
colored rectangles to indicate where a pattern is activated on the timeline """
def __init__ ( self , parentScene ) :
super ( ) . __init__ ( 0 , 0 , 1 , SIZE_UNIT )
self . parentScene = parentScene
self . statusMessage = self . parentScene . parentView . parentMainWindow . statusBar ( ) . showMessage #a version with the correct path of this is in every class of Patroneo
self . setAcceptHoverEvents ( True ) #for the preview highlight switch
self . exportDict = None #self.update gets called immediately after creation.
self . switches = { } # position:switchInstance
self . currentColor = None #set in updateMetaData
self . labelColor = None #set in updateMetaData for redable labels on our color. for example transpose number
#The track holds the horizontal lines. The number of barlines is calculated in the parentScene for all tracks at once.
self . topLine = QtWidgets . QGraphicsLineItem ( 0 , 0 , 1 , 0 ) #empty line is not possible. We need at least something to let self.update work with
self . topLine . setParentItem ( self )
self . topLine . setPos ( 0 , 0 )
self . bottomLine = QtWidgets . QGraphicsLineItem ( 0 , 0 , 1 , 0 ) #empty line is not possible. We need at least something to let self.update work with
self . bottomLine . setParentItem ( self )
self . bottomLine . setPos ( 0 , SIZE_UNIT )
self . topLine . setEnabled ( False )
self . bottomLine . setEnabled ( False )
#Interactive Marker to select several switches in a row
self . _mousePressOn = None #to remember the position of a mouse click
#self._markerLine = QtWidgets.QGraphicsLineItem(0,0,10,0) #only updated, never replaced
self . _markerLine = QtWidgets . QGraphicsRectItem ( 0 , 0 , 10 , SIZE_UNIT ) #only updated, never replaced
#self._markerLine.setParentItem(self) #incompatible with zValues. We need this relative to the scene
self . parentScene . addItem ( self . _markerLine )
#self._markerLine.setY(self.pos().y()) #won't work yet. We are not in the scene ourselves. We set it in mousePressEvent, before show()
self . _markerLine . setZValue ( _zValuesRelativeToScene [ " switch " ] + 1 ) #It is not possible to have this in front of barlines AND the switches. Barlines need to be below switches for multiplied-patterns. But we want to "erase" the switches
self . _markerLine . hide ( )
#Semitransparent hover-switch to show which one would be activated/deactivated
#Color and position is set in the callbacks and mouse handling
#It is below the actual switch so it will not show when there is already a switch, which is ok
self . _highlightSwitch = QtWidgets . QGraphicsRectItem ( 0 , 0 , SIZE_UNIT , SIZE_UNIT )
self . _highlightSwitch . setParentItem ( self )
self . _highlightSwitch . setOpacity ( 0.2 )
self . _highlightSwitch . hide ( )
def _setColors ( self , exportDict ) :
""" Called from various callbacks like updateSwitches and updateMetadata """
self . exportDict = exportDict
c = QtGui . QColor ( exportDict [ " color " ] )
self . currentColor = c
self . _highlightSwitch . setBrush ( c ) #this is with low opacity.
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 switches
def updateSwitches ( self , exportDict ) :
self . exportDict = exportDict
self . _setColors ( exportDict )
#Create new switches
for position in exportDict [ " structure " ] :
if not position in self . switches :
self . switches [ position ] = self . _createSwitch ( position )
factor = exportDict [ " patternLengthMultiplicator " ]
effectiveNumberOfMeasures = exportDict [ " numberOfMeasures " ] / / factor # //integer division
self . updateSwitchVisibility ( effectiveNumberOfMeasures )
def updateMetaData ( self , exportDict ) :
""" Color and Transposition status.
Does not get called on track structure change . """
self . exportDict = exportDict
self . _setColors ( exportDict )
for switch in self . switches . values ( ) :
switch . setBrush ( self . currentColor )
switch . setScaleTransposeColor ( self . labelColor )
switch . setHalftoneTransposeColor ( self . labelColor )
switch . setStepDelayColor ( self . labelColor )
switch . setAugmentationFactorColor ( self . labelColor )
def updatePatternLengthMultiplicator ( self , exportDict ) :
""" Comes via its own callback, also named callback_patternLengthMultiplicatorChanged.
The spinBox to set this is in TrackLabel """
self . updateSwitches ( exportDict ) # contains exportDict caching.
effectiveNumberOfMeasures = exportDict [ " numberOfMeasures " ] / / exportDict [ " patternLengthMultiplicator " ] # //integer division
#self.updateStaffLines(effectiveNumberOfMeasures) #we do not need to adjust the overall track length. That stays the same, no matter the factor.
def updateStaffLines ( self , requestAmountOfMeasures ) :
""" The two horizontal lines that mark our track.
We do NOT need to handle patternLengthMultiplicator since the overall track length
stays the same . Just the measure divisions are different .
"""
l = self . topLine . line ( )
l . setLength ( requestAmountOfMeasures * SIZE_UNIT )
self . topLine . setLine ( l )
l = self . bottomLine . line ( )
l . setLength ( requestAmountOfMeasures * SIZE_UNIT )
self . bottomLine . setLine ( l )
#Update self, which is the track background
self . setRect ( 0 , 0 , requestAmountOfMeasures * SIZE_UNIT , SIZE_UNIT )
def _createSwitch ( self , position ) :
""" Called only by self.updateSwitches
Qt can ' t put the same item into the scene twice. We need to create a new one each time " " "
switch = Switch ( parentTrackStructure = self , position = position )
assert self . currentColor
switch . setBrush ( self . currentColor )
#switch.setParentItem(self) #prevents zValue because switches are children of trackStructure. add to scene directly instead:
#For now we assume no stretch factor. One measure is one base measure.
#We set that in self.updateSwitchVisibility
self . scene ( ) . addItem ( switch )
switch . setPos ( position * SIZE_UNIT , self . y ( ) )
switch . setZValue ( _zValuesRelativeToScene [ " switch " ] )
return switch
def updateSwitchVisibility ( self , requestAmountOfMeasures ) :
""" Switch pattern-visibility on and off.
This never creates or deletes switches
We assume self . exportDict is up to date
because we get called by self . updateSwitches , which saves the exportDict . """
structure = self . exportDict [ " structure " ]
whichPatternsAreScaleTransposed = self . exportDict [ " whichPatternsAreScaleTransposed " ]
whichPatternsAreHalftoneTransposed = self . exportDict [ " whichPatternsAreHalftoneTransposed " ]
whichPatternsAreStepDelayed = self . exportDict [ " whichPatternsAreStepDelayed " ]
whichPatternsHaveAugmentationFactor = self . exportDict [ " whichPatternsHaveAugmentationFactor " ]
factor = self . exportDict [ " patternLengthMultiplicator " ]
#requestAmountOfMeasures = requestAmountOfMeasures // factor #already is.
#Adjust highlight mouse hover to new stretch factor
r = self . _highlightSwitch . rect ( )
r . setRight ( SIZE_UNIT * factor )
self . _highlightSwitch . setRect ( r )
vis = self . exportDict [ " visible " ]
for position , switch in self . switches . items ( ) :
#Deal with measures that stretch multiple base measures
switch . stretch ( factor )
switch . setPos ( position * SIZE_UNIT * factor , self . y ( ) )
#both position and requestAmountOfMeasures below are adjusted with scale factors
if not position in structure :
switch . hide ( ) #Not delete because this may be just a temporary reduction of measures
switch . scaleTransposeOff ( )
elif position > = requestAmountOfMeasures : #switch is out of bounds. For factor 1 this is the same as not in the score-area
switch . hide ( )
switch . scaleTransposeOff ( )
else :
switch . show ( )
if position in whichPatternsAreScaleTransposed :
switch . setScaleTranspose ( - 1 * whichPatternsAreScaleTransposed [ position ] ) #we flip the polarity from "makes sense" to row based "lower is higher" here. The opposite, sending, flip is done in switch hover leave event
else :
switch . scaleTransposeOff ( )
if position in whichPatternsAreHalftoneTransposed :
switch . setHalftoneTranspose ( whichPatternsAreHalftoneTransposed [ position ] ) #half tone transposition is not flipped
else :
switch . halftoneTransposeOff ( )
if position in whichPatternsAreStepDelayed :
switch . setStepDelay ( whichPatternsAreStepDelayed [ position ] )
else :
switch . stepDelayOff ( )
if position in whichPatternsHaveAugmentationFactor :
switch . setAugmentationFactor ( whichPatternsHaveAugmentationFactor [ position ] )
else :
switch . augmentationFactorOff ( )
if not vis :
switch . hide ( )
def contextMenuEvent ( self , event ) :
if self . _mousePressOn : #Right click can happen while the left button is still pressed down, which we don't want.
return
menu = QtWidgets . QMenu ( )
position = self . scenePos2switchPosition ( event . scenePos ( ) . x ( ) ) #measure number 0 based
position = position * self . exportDict [ " patternLengthMultiplicator " ] #We need the position in the global system, without the factor
measuresPerGroup = self . parentScene . measuresPerGroupCache
offset = position % measuresPerGroup
startMeasureForGroup = position - offset
endMeasureExclusive = startMeasureForGroup + measuresPerGroup
listOfLabelsAndFunctions = [
( QtCore . QCoreApplication . translate ( " SongStructure " , " Insert empty group before this one " ) , lambda : api . insertSilence ( howMany = measuresPerGroup , beforeMeasureNumber = startMeasureForGroup ) ) ,
( QtCore . QCoreApplication . translate ( " SongStructure " , " Exchange group with right neigbour " ) , lambda : api . exchangeSwitchGroupWithGroupToTheRight ( startMeasureForGroup , endMeasureExclusive ) ) ,
( QtCore . QCoreApplication . translate ( " SongStructure " , " Delete whole group " ) , lambda : api . deleteSwitches ( howMany = measuresPerGroup , fromMeasureNumber = startMeasureForGroup ) ) ,
( QtCore . QCoreApplication . translate ( " SongStructure " , " Duplicate whole group including measures " ) , lambda : api . duplicateSwitchGroup ( startMeasureForGroup , endMeasureExclusive ) ) ,
( QtCore . QCoreApplication . translate ( " SongStructure " , " Clear all group transpositions " ) , lambda : api . clearSwitchGroupModifications ( startMeasureForGroup , endMeasureExclusive ) ) ,
]
for text , function in listOfLabelsAndFunctions :
a = QtWidgets . QAction ( text , menu )
menu . addAction ( a )
a . triggered . connect ( function )
pos = QtGui . QCursor . pos ( )
pos . setY ( pos . y ( ) + 5 )
self . parentScene . parentView . parentMainWindow . setFocus ( )
menu . exec_ ( pos )
def scenePos2switchPosition ( self , x ) :
""" Map scene coordinates to counted switch engine position """
factor = self . exportDict [ " patternLengthMultiplicator " ]
return int ( x / SIZE_UNIT / factor )
def mousePressEvent ( self , event ) :
#First we need to find the mouse clicks position. self.switches only holds pos that were at least activated once.
#The track is only the area where the rectangles and lines meet. it is impossible to click below or right of the tracks.
#we always get a valid position this way.
if event . button ( ) == QtCore . Qt . MiddleButton and not self . _mousePressOn :
self . parentScene . parentView . parentMainWindow . patternGrid . createShadow ( self . exportDict )
else :
self . parentScene . parentView . parentMainWindow . chooseCurrentTrack ( self . exportDict , sendChangeToApi = True )
if event . button ( ) == QtCore . Qt . LeftButton : #Create a switch or continue to hold down mouse button and drag to draw -> mouseMoveEvent
assert not self . _mousePressOn
position = self . scenePos2switchPosition ( event . scenePos ( ) . x ( ) ) #measure number 0 based
self . _markerLine . setX ( position * SIZE_UNIT * self . exportDict [ " patternLengthMultiplicator " ] )
self . _markerLine . setY ( self . pos ( ) . y ( ) ) #we can't do that in init because self is not in the scene by then. markerLine is a child directly to the scene, no magic reparenting.
newBool = not position in self . switches or not self . switches [ position ] . isVisible ( )
if newBool :
self . _markerLine . setBrush ( self . currentColor )
else :
self . _markerLine . setBrush ( self . parentScene . parentView . parentMainWindow . fPalBlue . color ( QtGui . QPalette . AlternateBase ) ) #we are always the active track so this is our color
self . _mousePressOn = ( time ( ) , self , position , newBool ) #Reset to None in mouseReleaseEvent
result = api . setSwitch ( self . exportDict [ " id " ] , position , newBool ) #returns True if a switch happend
#elif event.button() == QtCore.Qt.RightButton and not self._mousePressOn:
#no, this is done with contextMenuEvent directly so it also reacts to the context menu keyboard key.
def mouseMoveEvent ( self , event ) :
""" Draw new switches by holding and dragging the mouse
This is only the visual aspect . Actual calcuation and insert is in mouseReleaseEvent
The initial _markerLine is setup in mousePressEvent , where the first switch is already set as well
In Patroneo this is only triggered when left mouse button is down .
We don ' t set the Qt flag to always react " " "
position = self . scenePos2switchPosition ( event . scenePos ( ) . x ( ) ) #measure number 0 based
factor = self . exportDict [ " patternLengthMultiplicator " ]
if self . _mousePressOn and position != self . _mousePressOn [ 2 ] :
#self._markerLine.setLine(0,0, (position - self._mousePressOn[2])*SIZE_UNIT + SIZE_UNIT/2, 0)
rect = self . _markerLine . rect ( )
if position < 0 :
position = 0
elif position + 1 > self . exportDict [ " numberOfMeasures " ] : #position is already a switch position
position = self . exportDict [ " numberOfMeasures " ] - 1
if position < self . _mousePressOn [ 2 ] :
left = ( position - self . _mousePressOn [ 2 ] ) * SIZE_UNIT
rect . setLeft ( left * factor )
rect . setRight ( SIZE_UNIT )
else :
right = ( position - self . _mousePressOn [ 2 ] ) * SIZE_UNIT + SIZE_UNIT
rect . setRight ( right * factor )
rect . setLeft ( 0 )
self . _markerLine . setRect ( rect )
self . _markerLine . show ( )
else :
self . _markerLine . hide ( )
def hoverEnterEvent ( self , event ) :
self . _highlightSwitch . show ( )
self . statusMessage ( QtCore . QCoreApplication . translate ( " Statusbar " , " Empty Measure: Left click to activate. Middle click to show as shadows in current pattern. Right click for measure group options. " ) ) #Yes, this is the track. Empty measures are not objects.
#This seemed to be a good idea but horrible UX. If you move the mouse down to edit a pattern you end up choosing the last track
#self.scene().parentView.parentMainWindow.chooseCurrentTrack(self.exportDict, sendChangeToApi=True)
def hoverLeaveEvent ( self , event ) :
self . statusMessage ( " " )
self . _highlightSwitch . hide ( )
def hoverMoveEvent ( self , event ) :
""" Snap the highlight switch to grid and stretch factor """
#x = round((event.scenePos().x() / SIZE_UNIT)-1) * SIZE_UNIT
switchPos = self . scenePos2switchPosition ( event . scenePos ( ) . x ( ) )
factor = self . exportDict [ " patternLengthMultiplicator " ]
x = switchPos * SIZE_UNIT * factor
self . _highlightSwitch . setX ( x )
def mouseReleaseEvent ( self , event ) :
if event . button ( ) == QtCore . Qt . LeftButton :
self . _markerLine . hide ( )
position = self . scenePos2switchPosition ( event . scenePos ( ) . x ( ) )
if position < 0 :
position = 0
elif position + 1 > self . exportDict [ " numberOfMeasures " ] : #position is already a switch position
position = self . exportDict [ " numberOfMeasures " ] - 1
startTime , startTrack , startPosition , setTo = self . _mousePressOn
self . _mousePressOn = None
if not startPosition == position and time ( ) - startTime > 0.4 : #optimisation to spare the engine from redundant work. Also prevent hectic drag-clicking
#setTo is a bool that tells us if all the switches in our range should go on (True) or off (False). The first switch, startPosition, is already set in mousePressEvent for a better user experience.
low , high = sorted ( ( startPosition , position ) ) #both included
setOfPositions = set ( range ( low , high + 1 ) ) #range does not include the last one, we want it in. it MUST be a set.
api . setSwitches ( self . exportDict [ " id " ] , setOfPositions , setTo )
def mark ( self , boolean ) :
""" Mark the whole Track as active or not.
This is directly for the switches . We also defer a call to the name label """
if boolean :
role = QtGui . QPalette . AlternateBase
else :
role = QtGui . QPalette . Base
c = self . parentScene . parentView . parentMainWindow . fPalBlue . color ( role )
self . setBrush ( c )
class Switch ( QtWidgets . QGraphicsRectItem ) :
""" Switches live for the duration of the track. Once created they only ever get hidden/shown,
never deleted .
Not every " empty square " has a switch already . Only switches that were activated at least once .
"""
def __init__ ( self , parentTrackStructure , position ) :
self . parentTrackStructure = parentTrackStructure
self . statusMessage = self . parentTrackStructure . parentScene . parentView . parentMainWindow . statusBar ( ) . showMessage #a version with the correct path of this is in every class of Patroneo
self . position = position
super ( ) . __init__ ( 0 , 0 , SIZE_UNIT , SIZE_UNIT )
#self.rect().setWidth(SIZE_UNIT)
self . setAcceptHoverEvents ( True )
self . scaleTransposeGlyph = QtWidgets . QGraphicsSimpleTextItem ( " " )
self . scaleTransposeGlyph . setParentItem ( self )
self . scaleTransposeGlyph . setScale ( 0.75 )
self . scaleTransposeGlyph . setPos ( 2 , 0 )
self . scaleTransposeGlyph . setBrush ( self . parentTrackStructure . labelColor )
self . scaleTransposeGlyph . hide ( )
self . scaleTranspose = 0 #default engine value, safe to assume that it will never change as default.
self . halftoneTransposeGlyph = QtWidgets . QGraphicsSimpleTextItem ( " " )
self . halftoneTransposeGlyph . setParentItem ( self )
self . halftoneTransposeGlyph . setScale ( 0.75 )
self . halftoneTransposeGlyph . setPos ( 2 , 0 ) #We expect that only one of the transpose variants will be used. Therefore we place them on the same coordinates, because there is not enough space for 4 mods.
self . halftoneTransposeGlyph . setBrush ( self . parentTrackStructure . labelColor )
self . halftoneTransposeGlyph . hide ( )
self . halftoneTranspose = 0 #default engine value, safe to assume that it will never change as default.
self . stepDelayGlyph = QtWidgets . QGraphicsSimpleTextItem ( " " )
self . stepDelayGlyph . setParentItem ( self )
self . stepDelayGlyph . setScale ( 0.75 )
self . stepDelayGlyph . setPos ( 1 , 10 )
self . stepDelayGlyph . setBrush ( self . parentTrackStructure . labelColor )
self . stepDelayGlyph . hide ( )
self . stepDelay = 0 #default engine value, safe to assume that it will never change as default.
self . augmentationFactorGlyph = QtWidgets . QGraphicsSimpleTextItem ( " " )
self . augmentationFactorGlyph . setParentItem ( self )
self . augmentationFactorGlyph . setScale ( 0.75 )
self . augmentationFactorGlyph . setPos ( 1 , 20 )
self . augmentationFactorGlyph . setBrush ( self . parentTrackStructure . labelColor )
self . augmentationFactorGlyph . hide ( )
self . augmentationFactor = 0 #default engine value, safe to assume that it will never change as default.
def stretch ( self , factor ) :
""" factor assumes relative to SIZE_UNIT """
r = self . rect ( )
r . setRight ( SIZE_UNIT * factor )
self . setRect ( r )
#Scale Transpose
def setScaleTranspose ( self , value ) :
"""
Called by track callbacks and also for the temporary buffer display
while internally both the engine and us , the GUI , use steps and transposition through
" negative is higher pitch " we present it reversed for the user .
Greater number is higher pitch
It is guaranteed that only active switches can have a transposition .
Also transposition = 0 is not included .
"""
self . scaleTranspose = value
self . _setScaleTransposeLabel ( value )
def _setScaleTransposeLabel ( self , value ) :
text = ( " + " if value > 0 else " " ) + str ( value ) + " s " #because - is added automatically
self . scaleTransposeGlyph . setText ( text )
self . scaleTransposeGlyph . show ( )
def setScaleTransposeColor ( self , c ) :
self . scaleTransposeGlyph . setBrush ( c )
def scaleTransposeOff ( self ) :
self . scaleTransposeGlyph . setText ( " " )
#self.scaleTransposeGlyph.hide()
self . scaleTranspose = 0
self . _bufferScaleTranspose = 0
def increaseScaleTranspose ( self ) :
""" By 1. Convenience function to make code in mainWindow cleaner """
self . _bufferScaleTranspose + = 1
self . _setScaleTransposeLabel ( self . _bufferScaleTranspose )
def decreaseScaleTranspose ( self ) :
""" By 1. Convenience function to make code in mainWindow cleaner """
self . _bufferScaleTranspose - = 1
self . _setScaleTransposeLabel ( self . _bufferScaleTranspose )
#Halftone Transpose
def setHalftoneTranspose ( self , value ) :
self . halftoneTranspose = value
self . _setHalftoneTransposeLabel ( value )
def _setHalftoneTransposeLabel ( self , value ) :
text = ( " + " if value > 0 else " " ) + str ( value ) + " h " #because - is added automatically
self . halftoneTransposeGlyph . setText ( text )
self . halftoneTransposeGlyph . show ( )
def setHalftoneTransposeColor ( self , c ) :
self . halftoneTransposeGlyph . setBrush ( c )
def halftoneTransposeOff ( self ) :
self . halftoneTransposeGlyph . setText ( " " )
#self.halftoneTransposeGlyph.hide()
self . halftoneTranspose = 0
self . _bufferHalftoneTranspose = 0
def increaseHalftoneTranspose ( self ) :
""" By 1. Convenience function to make code in mainWindow cleaner """
self . _bufferHalftoneTranspose + = 1
self . _setHalftoneTransposeLabel ( self . _bufferHalftoneTranspose )
def decreaseHalftoneTranspose ( self ) :
""" By 1. Convenience function to make code in mainWindow cleaner """
self . _bufferHalftoneTranspose - = 1
self . _setHalftoneTransposeLabel ( self . _bufferHalftoneTranspose )
#Step Delay
def setStepDelay ( self , value ) :
self . stepDelay = value
self . _setStepDelayLabel ( value )
def _setStepDelayLabel ( self , value ) :
text = ( " + " if value > 0 else " " ) + " d " + str ( value ) #because - is added automatically
self . stepDelayGlyph . setText ( text )
self . stepDelayGlyph . show ( )
def setStepDelayColor ( self , c ) :
self . stepDelayGlyph . setBrush ( c )
def stepDelayOff ( self ) :
self . stepDelayGlyph . setText ( " " )
#self.stepDelayGlyph.hide()
self . stepDelay = 0
self . _bufferStepDelay = 0
def increaseStepDelay ( self ) :
""" By 1. Convenience function to make code in mainWindow cleaner """
self . _bufferStepDelay + = 1
self . _setStepDelayLabel ( self . _bufferStepDelay )
def decreaseStepDelay ( self ) :
""" By 1. Convenience function to make code in mainWindow cleaner """
self . _bufferStepDelay - = 1
self . _setStepDelayLabel ( self . _bufferStepDelay )
#Augmentation Factor
def setAugmentationFactor ( self , value ) :
self . augmentationFactor = value
self . _setAugmentationFactorLabel ( Fraction ( value ) )
def _setAugmentationFactorLabel ( self , value ) :
text = " × " + str ( value )
self . augmentationFactorGlyph . setText ( text )
self . augmentationFactorGlyph . show ( )
def setAugmentationFactorColor ( self , c ) :
self . augmentationFactorGlyph . setBrush ( c )
def augmentationFactorOff ( self ) :
self . augmentationFactorGlyph . setText ( " " )
#self.augmentationFactorGlyph.hide()
self . augmentationFactor = 1.0
self . _bufferAugmentationFactor = 1.0
def increaseAugmentationFactor ( self ) :
""" By 1. Convenience function to make code in mainWindow cleaner """
self . _bufferAugmentationFactor * = 2
self . _setAugmentationFactorLabel ( Fraction ( self . _bufferAugmentationFactor ) )
def decreaseAugmentationFactor ( self ) :
""" By 1. Convenience function to make code in mainWindow cleaner """
self . _bufferAugmentationFactor / = 2
self . _setAugmentationFactorLabel ( Fraction ( self . _bufferAugmentationFactor ) )
#Events
def mousePressEvent ( self , event ) :
""" A mouse events on the track activate a switch. Then we receive the event to turn it
off again . """
event . ignore ( )
def hoverEnterEvent ( self , event ) :
""" Only active switches """
#self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Measure: Left click to deactivate. Middle click to show as shadows in current pattern. Shift+MouseWheel for half tone transposition. Alt+MouseWheel for in-scale transposition. Right click for measure group options."))
self . statusMessage ( QtCore . QCoreApplication . translate ( " Statusbar " , " Measure: Left click to deactivate. Middle click to show as shadows in current pattern. Right click for measure group options. Read the Edit menu for advanced modifications while hovering. " ) )
self . _bufferScaleTranspose = self . scaleTranspose
self . _bufferHalftoneTranspose = self . halftoneTranspose
self . _bufferStepDelay = self . stepDelay
self . _bufferAugmentationFactor = self . augmentationFactor
self . parentTrackStructure . parentScene . currentHoverStep = self
def hoverLeaveEvent ( self , event ) :
""" only triggered when active/shown.
When leaving a modified step it will send all changes to the api .
"""
self . parentTrackStructure . parentScene . currentHoverStep = None
self . statusMessage ( " " )
event . accept ( )
#The api callback resets our buffer and values.
#That is fine except if we want to register multiple changes at once. Therefore we first copy our buffers and send the copies.
bscale = - 1 * self . _bufferScaleTranspose
bhalftone = self . _bufferHalftoneTranspose
bdelay = self . _bufferStepDelay
baugment = self . _bufferAugmentationFactor
#Scale Transpose. Independent of Halftone Transpose
if not bscale == self . scaleTranspose :
api . setSwitchScaleTranspose ( self . parentTrackStructure . exportDict [ " id " ] , self . position , bscale ) #we flip the polarity here. The receiving flip is done in the callback.
#new transpose/buffer gets set via callback
if bscale == 0 :
self . scaleTransposeOff ( )
#Halftone Transpose. Independent of Scale Transpose
if not bhalftone == self . halftoneTranspose :
api . setSwitchHalftoneTranspose ( self . parentTrackStructure . exportDict [ " id " ] , self . position , bhalftone ) #half tone transposition is not flipped
#new transpose/buffer gets set via callback
if bhalftone == 0 :
self . halftoneTransposeOff ( )
#Step Delay. Also independent.
if not bdelay == self . stepDelay :
api . setSwitchStepDelay ( self . parentTrackStructure . exportDict [ " id " ] , self . position , bdelay )
#new value/buffer gets set via callback
if bdelay == 0 :
self . stepDelayOff ( )
#Augmentation Factor. Interconnected... nah, just joking. Independent of the other stuff.
if not baugment == self . augmentationFactor :
api . setSwitchAugmentationsFactor ( self . parentTrackStructure . exportDict [ " id " ] , self . position , baugment )
#new value/buffer gets set via callback
if baugment == 1.0 :
self . augmentationFactorOff ( )
def deprecated_wheelEvent ( self , event ) :
#We now use dedicated keyboard shortcuts and not the mousewheel anymore.
#See main window menu actions
""" Does not get triggered when switch is off.
This buffers until hoverLeaveEvent and then the new value is sent in self . hoverLeaveEvent
We want to keep normal scrolling with the mousewheel , therefore both transpose functions
need an additional key . Otherwise we get scroll on measures that are off and transpose
on measures that are active , which is very confusing .
"""
if QtWidgets . QApplication . keyboardModifiers ( ) == QtCore . Qt . ShiftModifier : #half tone transposition
event . accept ( )
if event . delta ( ) > 0 :
self . _bufferHalftoneTranspose = min ( + 24 , self . _bufferHalftoneTranspose + 1 )
else :
self . _bufferHalftoneTranspose = max ( - 24 , self . _bufferHalftoneTranspose - 1 )
self . _setHalftoneTransposeLabel ( self . _bufferHalftoneTranspose )
elif QtWidgets . QApplication . keyboardModifiers ( ) == QtCore . Qt . AltModifier : #scale transposition
event . accept ( )
if event . delta ( ) > 0 :
self . _bufferScaleTranspose = min ( + 7 , self . _bufferScaleTranspose + 1 )
else :
self . _bufferScaleTranspose = max ( - 7 , self . _bufferScaleTranspose - 1 )
self . _setScaleTransposeLabel ( self . _bufferScaleTranspose )
#Step Delay and Augmentation Factor are not done via mousewheel. There are not enough modifier keys left over :)
#They are instead handled by menu actions directly, in cooperations with our hover callbacks.
else : #normal scroll or zoom.
event . ignore ( )
#super.wheelEvent(event)
class TrackLabelEditor ( QtWidgets . QGraphicsScene ) :
""" Only the track labels: names, colors, groups """
def __init__ ( self , parentView ) :
super ( ) . __init__ ( )
self . parentView = parentView
self . statusMessage = self . parentView . parentMainWindow . statusBar ( ) . showMessage #a version with the correct path of this is in every class of Patroneo
self . tracks = { } #TrackID:TrackLabel
self . groups = [ ] #GroupLabel()
self . _cachedExportDictsInOrder = [ ]
self . _exportDictScore = None #cache
#Set color, otherwise it will be transparent in window managers or wayland that want that.
self . backColor = QtGui . QColor ( 55 , 61 , 69 )
self . backColor = QtGui . QColor ( 48 , 53 , 60 )
self . setBackgroundBrush ( self . backColor )
api . callbacks . numberOfTracksChanged . append ( self . callback_numberOfTracksChanged )
api . callbacks . trackMetaDataChanged . append ( self . callback_trackMetaDataChanged )
api . callbacks . numberOfMeasuresChanged . append ( self . callback_setnumberOfMeasures )
api . callbacks . exportCacheChanged . append ( self . cacheExportDict )
api . callbacks . patternLengthMultiplicatorChanged . append ( self . callback_patternLengthMultiplicatorChanged )
self . cachedCombinedTrackHeight = 0 #set by callback_tracksChanged
def cacheExportDict ( self , exportDict ) :
self . tracks [ exportDict [ " id " ] ] . exportDict = exportDict
def callback_trackMetaDataChanged ( self , exportDict ) :
""" This is not for the initial track creation, only for later changes """
self . tracks [ exportDict [ " id " ] ] . update ( exportDict )
def callback_setnumberOfMeasures ( self , exportDictScore ) :
self . _exportDictScore = exportDictScore
def callback_numberOfTracksChanged ( self , exportDictList ) :
groupsSeen = set ( ) #check if we already know this group
toDelete = set ( self . tracks . keys ( ) )
self . _cachedExportDictsInOrder = exportDictList
width = self . parentView . geometry ( ) . width ( )
#clean group labels. Will be recreated below
for group in self . groups :
self . removeItem ( group )
groupOffset = 0 #pixels. It is a positive/absolute value
hiddenOffsetCounter = 0 #counts hidden tracks in pixels. It is a positive/absolute value
for index , exportDict in enumerate ( exportDictList ) :
if exportDict [ " id " ] in self . tracks :
toDelete . remove ( exportDict [ " id " ] )
else : #new track
self . tracks [ exportDict [ " id " ] ] = TrackLabel ( parentScene = self , width = width , height = SIZE_UNIT )
self . addItem ( self . tracks [ exportDict [ " id " ] ] )
if exportDict [ " group " ] :
if not exportDict [ " group " ] in groupsSeen : #first encounter
groupsSeen . add ( exportDict [ " group " ] )
grouplabel = GroupLabel ( parentScene = self , width = width , height = SIZE_UNIT , name = exportDict [ " group " ] , visible = exportDict [ " visible " ] )
self . addItem ( grouplabel )
self . groups . append ( grouplabel )
grouplabel . setY ( index * SIZE_UNIT + SIZE_TOP_OFFSET + groupOffset - hiddenOffsetCounter ) #group offset is still "above" the current group. If this is a group itself the offset will be extended just belowt to make room for ourselves.
groupOffset = len ( groupsSeen ) * SIZE_UNIT
if exportDict [ " visible " ] :
self . tracks [ exportDict [ " id " ] ] . show ( )
else :
self . tracks [ exportDict [ " id " ] ] . hide ( )
hiddenOffsetCounter + = SIZE_UNIT
self . tracks [ exportDict [ " id " ] ] . setY ( index * SIZE_UNIT + SIZE_TOP_OFFSET + groupOffset - hiddenOffsetCounter )
self . tracks [ exportDict [ " id " ] ] . update ( exportDict )
#We had this tracks in the GUI but they are gone in the export. This is track delete.
for trackId in toDelete :
trackLabel = self . tracks [ trackId ]
del self . tracks [ trackId ]
self . removeItem ( trackLabel ) #remove from scene
del trackLabel
if toDelete and self . parentView . parentMainWindow . currentTrackId in toDelete : #toDelete still exist if tracks were deleted above
anyExistingTrack = next ( iter ( self . tracks . values ( ) ) )
self . parentView . parentMainWindow . chooseCurrentTrack ( anyExistingTrack . exportDict , sendChangeToApi = True )
self . cachedCombinedTrackHeight = len ( self . tracks ) * SIZE_UNIT + groupOffset - hiddenOffsetCounter
self . setSceneRect ( 0 , 0 , width - 1 , self . cachedCombinedTrackHeight + 3 * SIZE_TOP_OFFSET )
def callback_patternLengthMultiplicatorChanged ( self , exportDict ) :
self . tracks [ exportDict [ " id " ] ] . update ( exportDict ) #general update function that also covers our value
def tellApiToCreateNewGroupForTrack ( self , trackId ) :
title = QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Group Name " )
info = QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Create a new group by name " )
result , ok = QtWidgets . QInputDialog . getText ( self . parentView , title , info ) #parent, titlebar, info-text
if ok :
api . setTrackGroup ( trackId , str ( result ) )
def contextMenuEvent ( self , event ) :
"""
We can ' t delete this properly object from within. The engine callback will react faster
than we need to finish this function . That means qt and python will try to access
objects that are non - existent """
menu = QtWidgets . QMenu ( )
item = self . itemAt ( event . scenePos ( ) . x ( ) , event . scenePos ( ) . y ( ) , self . parentView . transform ( ) )
if not item :
return
if type ( item ) is TrackLabel :
exportDict = item . exportDict . copy ( )
elif type ( item . parentItem ( ) ) is TrackLabel :
exportDict = item . parentItem ( ) . exportDict . copy ( )
else :
return None
del item #yes, manual memory management in Python. We need to get rid of anything we want to delete later or Qt will crash because we have a python wrapper without a qt object
#Preare both on and off variants so we have a static string translation
stepDelayOnEntry = ( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Step Delay Wrap-Around: turn on " ) , lambda : api . changeTrackStepDelayWrapAround ( exportDict [ " id " ] , True ) )
stepDelayOffEntry = ( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Step Delay Wrap-Around: turn off " ) , lambda : api . changeTrackStepDelayWrapAround ( exportDict [ " id " ] , False ) )
stepDelayUse = stepDelayOffEntry if exportDict [ " stepDelayWrapAround " ] else stepDelayOnEntry
repeatDiminishedEntryOn = ( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Repeat Diminished Pattern in itself: turn on " ) , lambda : api . changeTrackRepeatDiminishedPatternInItself ( exportDict [ " id " ] , True ) )
repeatDiminishedEntryOff = ( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Repeat Diminished Pattern in itself: turn off " ) , lambda : api . changeTrackRepeatDiminishedPatternInItself ( exportDict [ " id " ] , False ) )
repeatDiminishedEntryUse = repeatDiminishedEntryOff if exportDict [ " repeatDiminishedPatternInItself " ] else repeatDiminishedEntryOn
listOfLabelsAndFunctions = [
( exportDict [ " sequencerInterface " ] [ " name " ] , None ) ,
( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Invert Measures " ) , lambda : api . trackInvertSwitches ( exportDict [ " id " ] ) ) ,
( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " All Measures On " ) , lambda : api . trackOnAllSwitches ( exportDict [ " id " ] ) ) ,
( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " All Measures Off " ) , lambda : api . trackOffAllSwitches ( exportDict [ " id " ] ) ) ,
stepDelayUse ,
repeatDiminishedEntryUse ,
( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Clone this Track " ) , lambda : api . createSiblingTrack ( exportDict [ " id " ] ) ) ,
( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Delete Track " ) , lambda : api . deleteTrack ( exportDict [ " id " ] ) ) ,
]
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 )
#Add a submenu for merge/cop
mergeMenu = menu . addMenu ( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Merge/Copy Measure-Structure from " ) )
def createCopyMergeLambda ( srcId ) :
return lambda : api . trackMergeCopyFrom ( srcId , exportDict [ " id " ] )
for sourceDict in self . _cachedExportDictsInOrder :
a = QtWidgets . QAction ( sourceDict [ " sequencerInterface " ] [ " name " ] , mergeMenu )
mergeMenu . addAction ( a )
mergeCommand = createCopyMergeLambda ( sourceDict [ " id " ] )
if sourceDict [ " id " ] == exportDict [ " id " ] :
a . setEnabled ( False )
a . triggered . connect ( mergeCommand )
#Add a submenu for pattern merge/copy
copyMenu = menu . addMenu ( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Replace Pattern with " ) )
def replacePatternWithLambda ( srcId ) :
return lambda : api . trackPatternReplaceFrom ( srcId , exportDict [ " id " ] )
for sourceDict in self . _cachedExportDictsInOrder :
a = QtWidgets . QAction ( sourceDict [ " sequencerInterface " ] [ " name " ] , copyMenu )
copyMenu . addAction ( a )
mergeCommand = replacePatternWithLambda ( sourceDict [ " id " ] )
if sourceDict [ " id " ] == exportDict [ " id " ] :
a . setEnabled ( False )
a . triggered . connect ( mergeCommand )
#Add a submenu to set the midi channel of this track. Highlight the current one
midiChannelMenu = menu . addMenu ( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Send on MIDI Channel " ) )
for mch in range ( 1 , 17 ) :
mchAction = QtWidgets . QAction ( str ( mch ) , midiChannelMenu )
midiChannelMenu . addAction ( mchAction )
midiChannelCommand = lambda discard , chArg = mch : api . changeTrackMidiChannel ( exportDict [ " id " ] , chArg ) #discard parameter given by QAction
if exportDict [ " midiChannel " ] == mch :
mchAction . setEnabled ( False )
mchAction . triggered . connect ( midiChannelCommand )
#Add a submenu for track groups. Will call the api which will send us a callback to reorder the tracks.
groupMenu = menu . addMenu ( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Group " ) )
newGroupAction = QtWidgets . QAction ( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " New Group " ) , groupMenu )
groupMenu . addAction ( newGroupAction )
newGroupAction . triggered . connect ( lambda : self . tellApiToCreateNewGroupForTrack ( exportDict [ " id " ] ) )
if exportDict [ " group " ] :
removeGroupAction = QtWidgets . QAction ( QtCore . QCoreApplication . translate ( " TrackLabelContext " , " Remove from " ) + exportDict [ ' group ' ] , groupMenu )
groupMenu . addAction ( removeGroupAction )
removeGroupAction . triggered . connect ( lambda : api . setTrackGroup ( exportDict [ " id " ] , " " ) ) #empty string = no group
groupMenu . addSeparator ( )
#Offer existing groups
for groupString in api . getGroups ( ) :
grpAction = QtWidgets . QAction ( groupString , groupMenu )
groupMenu . addAction ( grpAction )
midiChannelCommand = lambda discard , grpArg = groupString : api . setTrackGroup ( exportDict [ " id " ] , grpArg ) #discard parameter given by QAction
if exportDict [ " group " ] == groupString :
grpAction . setEnabled ( False )
grpAction . triggered . connect ( midiChannelCommand )
pos = QtGui . QCursor . pos ( )
pos . setY ( pos . y ( ) + 5 )
self . parentView . parentMainWindow . setFocus ( )
menu . exec_ ( pos )
class TrackLabel ( QtWidgets . QGraphicsRectItem ) :
""" One track label with color, name line edit etc.
Only gets the data when update ( ) is called .
"""
def __init__ ( self , parentScene , width , height ) :
super ( ) . __init__ ( 0 , 0 , width , height )
self . parentScene = parentScene
self . exportDict = None #set in self.update
self . statusMessage = self . parentScene . parentView . parentMainWindow . statusBar ( ) . showMessage #a version with the correct path of this is in every class of Patroneo
self . setPen ( QtGui . QPen ( QtCore . Qt . NoPen ) )
self . setFlag ( self . ItemIgnoresTransformations ) #zoom will repostion but not make the font bigger.
#Child widgets. SIZE_UNIT is used as positioning block. Multiply yourself :)
self . positioningHandle = TrackLabel . PositioningHandle ( parentTrackLabel = self )
self . positioningHandle . setParentItem ( self )
self . lengthMultiplicatorSpinBox = TrackLabel . lengthMultiplicatorSpinBox ( parentTrackLabel = self )
self . lengthMultiplicatorSpinBox . setParentItem ( self )
self . colorButton = TrackLabel . ColorPicker ( parentTrackLabel = self )
self . colorButton . setParentItem ( self )
self . lineEdit = TrackLabel . NameLineEdit ( parentTrackLabel = self )
self . label = QtWidgets . QGraphicsProxyWidget ( )
self . label . setWidget ( self . lineEdit )
self . label . setParentItem ( self )
self . positionButtons ( )
def positionButtons ( self , inGroup : bool = False ) :
""" Used for init, but also for update to show if we are in a track-group or not by
indentation . """
if inGroup :
offsetInSizeUnit = SIZE_UNIT
else :
offsetInSizeUnit = 0
self . positioningHandle . setPos ( 0 , 0 )
self . lengthMultiplicatorSpinBox . setPos ( SIZE_UNIT , 2 )
self . colorButton . setPos ( 3 * SIZE_UNIT + offsetInSizeUnit , 3 )
self . label . setPos ( 4 * SIZE_UNIT + 3 + offsetInSizeUnit , 0 )
class lengthMultiplicatorSpinBox ( QtWidgets . QGraphicsProxyWidget ) :
def __init__ ( self , parentTrackLabel ) :
super ( ) . __init__ ( )
self . parentTrackLabel = parentTrackLabel
self . setAcceptHoverEvents ( True )
self . spinBox = QtWidgets . QSpinBox ( )
self . spinBox . setSuffix ( " x " )
#self.spinBox.setFrame(True)
self . spinBox . setMinimum ( 1 )
self . setWidget ( self . spinBox )
self . spinBox . valueChanged . connect ( self . spinBoxValueChanged ) #Callback for setting is in ParentTrackLabel.update
def hoverEnterEvent ( self , event ) :
self . parentTrackLabel . statusMessage ( QtCore . QCoreApplication . translate ( " Statusbar " , " Measure length multiplicator. Enter number or spin the mouse wheel to change. " ) )
def hoverLeaveEvent ( self , event ) :
self . parentTrackLabel . statusMessage ( " " )
def spinBoxValueChanged ( self ) :
self . parentTrackLabel . parentScene . parentView . parentMainWindow . chooseCurrentTrack ( self . parentTrackLabel . exportDict , sendChangeToApi = True )
api . setTrackPatternLengthMultiplicator ( self . parentTrackLabel . exportDict [ " id " ] , self . spinBox . value ( ) )
class ColorPicker ( QtWidgets . QGraphicsRectItem ) :
def __init__ ( self , parentTrackLabel ) :
super ( ) . __init__ ( 0 , 0 , SIZE_UNIT * 0.75 , SIZE_UNIT * 0.75 )
self . parentTrackLabel = parentTrackLabel
self . setBrush ( QtGui . QColor ( " cyan " ) )
self . setAcceptHoverEvents ( True )
self . setToolTip ( QtCore . QCoreApplication . translate ( " TrackLabel " , " change track color " ) )
def hoverEnterEvent ( self , event ) :
self . parentTrackLabel . statusMessage ( QtCore . QCoreApplication . translate ( " Statusbar " , " Left click to change track color " ) )
def hoverLeaveEvent ( self , event ) :
self . parentTrackLabel . statusMessage ( " " )
def mousePressEvent ( self , event ) :
if event . button ( ) == QtCore . Qt . LeftButton :
self . parentTrackLabel . parentScene . parentView . parentMainWindow . chooseCurrentTrack ( self . parentTrackLabel . exportDict , sendChangeToApi = True )
event . accept ( )
colorDialog = QtWidgets . QColorDialog ( )
color = colorDialog . getColor ( self . brush ( ) . color ( ) ) #blocks
if color . isValid ( ) : #and not abort
#self.setBrush(color) #done via callback.
api . changeTrackColor ( self . parentTrackLabel . exportDict [ " id " ] , color . name ( ) )
#else:
# colorDialog.setStandardColor(self.brush().color())
else :
event . ignore ( )
#super().mousePressEvent(event)
class PositioningHandle ( QtWidgets . QGraphicsEllipseItem ) :
def __init__ ( self , parentTrackLabel ) :
super ( ) . __init__ ( 0 , 0 , SIZE_UNIT - 2 , SIZE_UNIT - 2 )
self . setToolTip ( QtCore . QCoreApplication . translate ( " TrackLabel " , " grab and move to reorder tracks " ) )
self . parentTrackLabel = parentTrackLabel
self . setPen ( QtGui . QPen ( QtCore . Qt . NoPen ) )
role = QtGui . QPalette . ToolTipBase
c = self . parentTrackLabel . parentScene . parentView . parentMainWindow . fPalBlue . color ( role )
self . setBrush ( c )
self . setOpacity ( 0.08 ) #this is meant as a slight overlay/highlight of both the current track and the other tracks
self . arrowLabel = QtWidgets . QGraphicsSimpleTextItem ( " ↕ " )
self . arrowLabel . setFlag ( QtWidgets . QGraphicsItem . ItemIgnoresParentOpacity )
self . arrowLabel . setParentItem ( self )
self . arrowLabel . setScale ( 1.5 )
self . arrowLabel . setPos ( 5 , 2 ) #try to get the center
role = QtGui . QPalette . Text
self . arrowLabel . setBrush ( self . parentTrackLabel . parentScene . parentView . parentMainWindow . fPalBlue . color ( role ) )
self . _cursorPosOnMoveStart = None
self . setAcceptHoverEvents ( True )
def hoverEnterEvent ( self , event ) :
self . parentTrackLabel . statusMessage ( QtCore . QCoreApplication . translate ( " Statusbar " , " Hold left mouse button and move to reorder tracks " ) )
def hoverLeaveEvent ( self , event ) :
self . parentTrackLabel . statusMessage ( " " )
def yPos2trackIndex ( self , y ) :
""" 0 based """
pos = round ( y / SIZE_UNIT )
pos = min ( pos , len ( self . parentTrackLabel . parentScene . tracks ) - 1 )
return pos
def mouseMoveEvent ( self , event ) :
""" Move track up and down. Started by mousePressEvent and finalized by mouseReleaseEvent """
if self . _cursorPosOnMoveStart :
self . parentTrackLabel . setY ( max ( 0 , event . scenePos ( ) . y ( ) ) )
#super().mouseMoveEvent(event) #with this the sync between cursor and item is off.
def mousePressEvent ( self , event ) :
""" release gets only triggered when mousePressEvent was on the same item.
We don ' t need to worry about the user just releasing the mouse on this item " " "
self . _posBeforeMove = self . parentTrackLabel . pos ( )
self . _cursorPosOnMoveStart = QtGui . QCursor . pos ( )
self . parentTrackLabel . setZValue ( self . parentTrackLabel . zValue ( ) + 1 ) #in front of other tracks
self . _lineCursor = self . parentTrackLabel . lineEdit . cursor ( )
self . parentTrackLabel . mousePressEvent ( event )
#super().mousePressEvent(event) #with this in mouseMoveEvent does not work. IIRC because we do not set the movableFlag
def mouseReleaseEvent ( self , event ) :
newIndex = self . yPos2trackIndex ( self . parentTrackLabel . y ( ) ) #we need to save that first, right after this we reset the position
self . parentTrackLabel . setPos ( self . _posBeforeMove ) #In case the block was moved to a position where no track is (below the tracks) we just reset the graphics before anything happens. The user will never see this really
self . parentTrackLabel . setZValue ( self . parentTrackLabel . zValue ( ) - 1 ) #revert mousePressEvent's +1
self . _posBeforeMove = None
self . _cursorPosOnMoveStart = None
api . moveTrack ( self . parentTrackLabel . exportDict [ " id " ] , newIndex )
class NameLineEdit ( QtWidgets . QLineEdit ) :
def __init__ ( self , parentTrackLabel ) :
super ( ) . __init__ ( " " )
self . parentTrackLabel = parentTrackLabel
self . setFrame ( False )
self . setMaxLength ( 25 )
self . setMinimumSize ( QtCore . QSize ( 0 , SIZE_UNIT ) )
self . setStyleSheet ( " background-color: rgba(0,0,0,0) " ) #transparent so we see the RectItem color
self . setReadOnly ( True )
self . setFocusPolicy ( QtCore . Qt . ClickFocus ) #nmo tab
self . editingFinished . connect ( self . sendToEngine )
self . returnPressed . connect ( self . enter )
def enterEvent ( self , event ) :
self . parentTrackLabel . statusMessage ( QtCore . QCoreApplication . translate ( " Statusbar " , " Click to select track. Double click to change track name " ) )
def leaveEvent ( self , event ) :
self . parentTrackLabel . statusMessage ( " " )
def mousePressEvent ( self , event ) :
""" We also need to force this track as active """
event . accept ( ) #we need this for doubleClick
self . parentTrackLabel . parentScene . parentView . parentMainWindow . chooseCurrentTrack ( self . parentTrackLabel . exportDict , sendChangeToApi = True )
#event.ignore() #send to parent instead
#super().mousePressEvent(event)
def mouseDoubleClickEvent ( self , event ) :
event . accept ( )
self . setReadOnly ( False )
def enter ( self ) :
self . sendToEngine ( )
def sendToEngine ( self ) :
self . setReadOnly ( True )
new = self . text ( )
if not new == self . parentTrackLabel . exportDict [ " sequencerInterface " ] [ " name " ] :
self . blockSignals ( True )
api . changeTrackName ( self . parentTrackLabel . exportDict [ " id " ] , new )
self . blockSignals ( False )
#def keyPressEvent(self, event):
# if event.key()) == QtCore.Qt.Key_Return:
# event.accept()
#
# else:
# event.ignore()
# super().keyPressEvent(event)
def update ( self , exportDict ) :
self . exportDict = exportDict
self . positionButtons ( inGroup = bool ( exportDict [ " group " ] ) )
self . lineEdit . setText ( exportDict [ " sequencerInterface " ] [ " name " ] )
self . colorButton . setBrush ( QtGui . QColor ( exportDict [ " color " ] ) )
self . lengthMultiplicatorSpinBox . spinBox . blockSignals ( True )
self . lengthMultiplicatorSpinBox . spinBox . setValue ( int ( exportDict [ " patternLengthMultiplicator " ] ) )
self . lengthMultiplicatorSpinBox . spinBox . blockSignals ( False )
def mousePressEvent ( self , event ) :
self . parentScene . parentView . parentMainWindow . chooseCurrentTrack ( self . exportDict , sendChangeToApi = True )
#event.ignore() #don't add that here. Will destroy mouseMove and Release events of child widgets
#def mouseReleaseEvent(self, event):
# event.
def mark ( self , boolean ) :
if boolean :
role = QtGui . QPalette . AlternateBase
else :
role = QtGui . QPalette . Base
c = self . parentScene . parentView . parentMainWindow . fPalBlue . color ( role )
self . setBrush ( c )
class GroupLabel ( QtWidgets . QGraphicsRectItem ) :
""" Compatible with TrackLabel but stripped down:
No name change , no color , no multiplicator . But
a name that you can drag around
Group Labels get deleted and recreated on each change .
"""
def __init__ ( self , parentScene , width , height , name , visible ) :
super ( ) . __init__ ( 0 , 0 , width , height )
self . parentScene = parentScene
self . statusMessage = self . parentScene . parentView . parentMainWindow . statusBar ( ) . showMessage #a version with the correct path of this is in every class of Patroneo
self . group = name
self . visible = visible #if that changes it will change only on creation of a GroupLabel instance
self . setPen ( QtGui . QPen ( QtCore . Qt . NoPen ) )
self . setFlag ( self . ItemIgnoresTransformations ) #zoom will repostion but not make the font bigger.
c = self . parentScene . parentView . parentMainWindow . fPalBlue . color ( QtGui . QPalette . Window )
self . setBrush ( c )
#Child widgets. SIZE_UNIT is used as positioning block. Multiply yourself :)
self . _duringGroupMove = False
self . positioningHandle = GroupLabel . PositioningHandle ( parentGroupLabel = self )
self . positioningHandle . setParentItem ( self )
self . positioningHandle . setPos ( 0 , 0 )
if visible :
name = " ▼ " + name
else :
name = " ▶ " + name
self . qLabel = QtWidgets . QLabel ( name )
self . label = QtWidgets . QGraphicsProxyWidget ( )
self . label . setWidget ( self . qLabel )
self . label . setParentItem ( self )
self . label . setPos ( 3 * SIZE_UNIT + 3 , 0 )
self . qLabel . setMinimumSize ( QtCore . QSize ( 0 , SIZE_UNIT ) )
self . qLabel . setStyleSheet ( " background-color: rgba(0,0,0,0) " ) #transparent so we see the RectItem color
self . setAcceptHoverEvents ( True )
def hoverEnterEvent ( self , event ) :
self . statusMessage ( QtCore . QCoreApplication . translate ( " Statusbar " , " Track Group: Double Click to show or hide. You can also double click the empty group spacers above the tracks. " ) )
def hoverLeaveEvent ( self , event ) :
self . statusMessage ( " " )
def mouseDoubleClickEvent ( self , event ) :
""" Without this no PositionHandle mouseMove and mouse Release events!!!
Also no double click """
#super().mousePressEvent(event)
if not self . positioningHandle . _cursorPosOnMoveStart : #during group move
api . setGroupVisible ( self . group ) #calling with one parameter toggles visibility.
#def mouseDoubleClickEvent(self, event):
# event.accept()
class PositioningHandle ( QtWidgets . QGraphicsEllipseItem ) :
def __init__ ( self , parentGroupLabel ) :
super ( ) . __init__ ( 0 , 0 , SIZE_UNIT - 2 , SIZE_UNIT - 2 )
self . setToolTip ( QtCore . QCoreApplication . translate ( " GroupLabel " , " grab and move to reorder groups " ) )
self . parentGroupLabel = parentGroupLabel
self . setPen ( QtGui . QPen ( QtCore . Qt . NoPen ) )
role = QtGui . QPalette . ToolTipBase
c = self . parentGroupLabel . parentScene . parentView . parentMainWindow . fPalBlue . color ( role )
self . setBrush ( c )
self . setOpacity ( 0.08 ) #this is meant as a slight overlay/highlight of both the current track and the other tracks
self . arrowLabel = QtWidgets . QGraphicsSimpleTextItem ( " ↕ " )
self . arrowLabel . setFlag ( QtWidgets . QGraphicsItem . ItemIgnoresParentOpacity )
self . arrowLabel . setParentItem ( self )
self . arrowLabel . setScale ( 1.5 )
self . arrowLabel . setPos ( 5 , 2 ) #try to get the center
role = QtGui . QPalette . Text
self . arrowLabel . setBrush ( self . parentGroupLabel . parentScene . parentView . parentMainWindow . fPalBlue . color ( role ) )
#self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, bool) #!!! Prevents double click to hide.
#self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, bool) #!!! Prevents double click to hide.
self . _cursorPosOnMoveStart = None
self . setAcceptHoverEvents ( True )
def hoverEnterEvent ( self , event ) :
self . parentGroupLabel . statusMessage ( QtCore . QCoreApplication . translate ( " Statusbar " , " Hold left mouse button and move to reorder track groups " ) )
def hoverLeaveEvent ( self , event ) :
self . parentGroupLabel . statusMessage ( " " )
def yPos2trackIndex ( self , y ) :
""" 0 based """
pos = round ( y / SIZE_UNIT )
pos = min ( pos , len ( self . parentGroupLabel . parentScene . tracks ) - 1 )
return pos
def mouseMoveEvent ( self , event ) :
if self . _cursorPosOnMoveStart :
self . parentGroupLabel . setY ( max ( 0 , event . scenePos ( ) . y ( ) ) )
#super().mouseMoveEvent(event) #with this the sync between cursor and item is off.
def mousePressEvent ( self , event ) :
""" release gets only triggered when mousePressEvent was on the same item.
We don ' t need to worry about the user just releasing the mouse on this item " " "
self . _posBeforeMove = self . parentGroupLabel . pos ( )
self . _cursorPosOnMoveStart = QtGui . QCursor . pos ( )
self . parentGroupLabel . setZValue ( self . parentGroupLabel . zValue ( ) + 1 ) #in front of other groups
#self.parentGroupLabel.mousePressEvent(event) #This blocks mouseMOveEvent
#super().mousePressEvent(event) #with this in mouseMoveEvent does not work. IIRC because we do not set the movableFlag
def mouseReleaseEvent ( self , event ) :
newIndex = self . yPos2trackIndex ( self . parentGroupLabel . y ( ) ) #we need to save that first, right after this we reset the position
self . parentGroupLabel . setPos ( self . _posBeforeMove ) #In case the block was moved to a position where no track is (below the tracks) we just reset the graphics before anything happens. The user will never see this really
self . parentGroupLabel . setZValue ( self . parentGroupLabel . zValue ( ) - 1 ) #revert mousePressEvent's + 1
self . _posBeforeMove = None
self . _cursorPosOnMoveStart = None
api . moveGroup ( self . parentGroupLabel . group , newIndex )
class Playhead ( QtWidgets . QGraphicsLineItem ) :
def __init__ ( self , parentScene ) :
super ( ) . __init__ ( 0 , 0 , 0 , 0 ) # (x1, y1, x2, y2)
self . parentScene = parentScene
p = QtGui . QPen ( )
p . setColor ( QtGui . QColor ( " red " ) )
p . setWidth ( 3 )
#PlayHead height is set in SongEditor.callback_numberOfTracksChanged.
#p.setCosmetic(True)
self . setPen ( p )
api . callbacks . setPlaybackTicks . append ( self . setCursorPosition )
self . setZValue ( _zValuesRelativeToScene [ " playhead " ] )
def setCursorPosition ( self , tickindex , playbackStatus ) :
""" Set the playhead to the right position, but keep the viewport stable.
Shift the entire " page " if the cursor becomes invisible because its steps outside the viewport .
PlayHead height is set in SongEditor . callback_numberOfTracksChanged .
"""
x = int ( tickindex / self . parentScene . ticksToPixelRatio )
self . setX ( x )
if playbackStatus : # api.duringPlayback:
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 )