self.hiddenTracks={}#track-instance:original Position. The value can exist multiple times. These still create a playback representation but are read-only and do not react to editing or GUI requests because you can't access them except through Track.allTrack (which is exactly the same for deleted tracks). Hidden tracks are saved though, deleted ones not.
self.tempoTrack=TempoTrack(parentData=self)#The tempoTrack is a Laborejo class. TempoMap is a template.sequencer class that is used by TempoTrack internally
self.cursorWhenSelectionStarted=None#A cursor dict, from self.cursorExport(). Is None when there is no selection. Can be used for "if selection:" questions. Gets changed quite often.
self.copyObjectsBuffer=[]#for copy and paste. obviously empty after load file. Also not saved.
self.cachedTrackDurations={}#updated after every track export
#track.asMetronomeData is a generated value from staticExport. Not available yet. needs to be done in api.startEngine #self.metronome.generate(data=self.currentMetronomeTrack.asMetronomeData, label=self.currentMetronomeTrack.name)
trackList=list(set([block.parentTrackforblockincurBlock.linkedContentBlocksInScore()]))#for this list(set( it doesn't matter that the trackList will not be in order.
returntrackList,curBlock
definsertTrack(self,atIndex,trackObject):
"""This is also newTrack. the api creates a track object for that.
ifself.trackIndex+1>len(self.tracks):#trackIndex from 0, len from 1.
self.trackIndex-=1
self.currentTrack().head()
self.currentTrack().goToTickindex(nowInTicks)#that is already the new current track
returnresult
defdeleteTrack(self,track):
iftrack==self.currentTrack():
returnself.deleteCurrentTrack()#redirect. This way we have no problems with cursor positions etc.
iflen(self.tracks)>1:
#We need to know if the old current track was AFTER the deleted one. In this case the trackindex needs to get subtracted by one (before the track is actually deleted)
result=self.currentTrack(),self.trackIndex#so undo has something to save
ifself.trackIndex>self.tracks.index(track):#there is only > or <. == is ruled out above.
self.trackIndex-=1
super().deleteTrack(track)
#track.prepareForDeletion()
#self.tracks.remove(track)
returnresult
defhideTrack(self,track):
"""This track cannot be edited while hidden.
Wedon't delete any calfbox data here, contrary to delete Track.
Thatmeanswedon't need to touch anything and the playback
versionwillremaincorrect.
Alsomakesureweareintherighttrackafterwardssince
hideTrackchangesthetrackIndices"""
asserttrackinself.tracks#track is visible so it should be in the list of visible tracks
assertnottrackinself.hiddenTracks#track is visible so it should not be in the list of hidden tracks
iflen(self.tracks)>1:#same rules as for delete. do not hide the last visible track
self.hiddenTracks[track]=self.tracks.index(track)
ifself.trackIndex==self.tracks.index(track):#the track with the active cursor gets hidden
self.tracks.remove(track)
self.trackIndex=0
else:
currentTrack=self.tracks[self.trackIndex]
self.tracks.remove(track)
self.trackIndex=self.tracks.index(currentTrack)#find out where the current track is now.
returnTrue#succesful
defunhideTrack(self,track):
assertnottrackinself.tracks#track is hidden so it should not be in the list of visible tracks
asserttrackinself.hiddenTracks#track is hidden so it should be in the list of hidden tracks
track.toPosition(originalPosition)#has head() in it
returnexportCursor
defsetSelectionBeginning(self):
ifself.cursorWhenSelectionStarted:
returnFalse#because we did nothing
else:
self.cursorWhenSelectionStarted=self.cursorExport()#There was no selection before. setSelectionBeginning is called by selection-creating functions. So this was the first step of shift+right or so.
raiseRuntimeError("called score.goToSelectionEnd without a selection")
else:
topLeftCursor,bottomRightCursor=result
#The cursor when selection started is always the opposite of the current cursor position. except if the selection has zero width, which is ruled out by the test above.
ifself.cursorWhenSelectionStarted==topLeftCursor:#already at the end
self.goTo(bottomRightCursor["trackIndex"],bottomRightCursor["blockindex"],bottomRightCursor["localCursorIndex"])#does not work if the number of items change. e.g. split
returnTrue
#else: This fires when the selection goes from bottomLeft to topRight or reverse. Make the "elif" above the new "else".
#raise ValueError("Cursor was neither at the start nor the end of the selection but a selection was present. This is a serious error. Only selections with the cursor on either end are allowed")
defgoToSelectionStart(self):
result=self.checkSelectionOrder()
ifnotresult:
raiseRuntimeError("called score.goToSelectionEnd without a selection")
else:
topLeftCursor,bottomRightCursor=result#one of these is always the current cursor, not a cached value. You don't need to test if the current cursor is actually one of these two.
#The cursor when selection started is always the opposite of the current cursor position. except if the selection has zero width, which is ruled out by the test above.
ifself.cursorWhenSelectionStarted==bottomRightCursor:#already at the beginning
self.goTo(topLeftCursor["trackIndex"],topLeftCursor["blockindex"],topLeftCursor["localCursorIndex"])#contrary to goToSelectionEnd this works always. No duration or number change can affect this position.
returnTrue
#else: This fires when the selection goes from bottomLeft to topRight or reverse. Make the "elif" above the new "else".
# raise ValueError("Cursor was neither at the start nor the end of the selection but a selection was present. This is a serious error. Only selections with the cursor on either end are allowed")
defcheckSelectionOrder(self):
"""Return the correct order of the selection boundaries
(topLeft,bottomRight)
Returnsemptytupleifwedon't have a selection or
aselectionofsize0."""
ifself.cursorWhenSelectionStarted:#is None if we don't have a selection at all. Otherwise a cursor export object like self.cursorExport()
#The only time this should happen is after a selection modification by the user which results in having no selections. e.g. "select right", "select left".
#A special case if a selection changes direction, or "inverts".
#Note that we don't reset self.cursorWhenSelectionStarted. The selection process is still going on.
returntuple()
#we have a selection with dimensions > 0.
ifself.cursorWhenSelectionStarted["trackIndex"]<current["trackIndex"]:#selection order GUI-view: top to bottom
ifself.cursorWhenSelectionStarted["tickindex"]<current["tickindex"]:#selection order left to right
result=self.cursorWhenSelectionStarted,current
else:#selection order right to left. Both topLeft and bottomRight are not created by the user directly. We have to guess what start item the user wants which can be hard if zero-duration items are involved.
#There is no other way to go the tickindex of the real first item and force a cursor export.
topLeft=self._cursorExportTrackTick(self.cursorWhenSelectionStarted["track"],current["tickindex"])#yes, the tickindex of the other track
bottomRight=self._cursorExportTrackTick(current["track"],self.cursorWhenSelectionStarted["tickindex"])#yes, the tickindex of the other track
returntopLeft,bottomRight
elifself.cursorWhenSelectionStarted["trackIndex"]>current["trackIndex"]:#selection order GUI-view: bottom to top
ifself.cursorWhenSelectionStarted["tickindex"]>current["tickindex"]:#selection order left to right
result=current,self.cursorWhenSelectionStarted
else:#selection order right to left. Both topLeft and bottomRight are not created by the user directly. We have to guess what start item the user wants which can be hard if zero-duration items are involved.
#There is no other way to go the tickindex of the real first item and force a cursor export.
bottomRight=self._cursorExportTrackTick(self.cursorWhenSelectionStarted["track"],current["tickindex"])#yes, the tickindex of the other track
topLeft=self._cursorExportTrackTick(current["track"],self.cursorWhenSelectionStarted["tickindex"])#yes, the tickindex of the other track
returntopLeft,bottomRight
else:#in the same track
assertself.cursorWhenSelectionStarted["trackIndex"]==current["trackIndex"]#anything else is logically impossible.
return[False,None,None,[],[]]#mimickes the true result. the api can use this to decide to do nothing instead of processing the selection
else:
selectionValid,topLeft,bottomRight=t
ifnotselectionValid:
return[False,topLeft,bottomRight,[],[]]#mimics the result format. the api can use this to decide to do nothing instead of processing the selection
startTick=topLeft["tickindex"]
endTick=bottomRight["tickindex"]
seenItems=set()#keep duplicate items, aka. content link block data, out of the result.
tracksWithSelectedItems=[trackfortrackinself.tracks[topLeft["trackIndex"]:bottomRight["trackIndex"]+1]]#slice does not include the last value, so +1
#listOfChangedTrackIds = set([id(track) for track in tracksWithSelectedItems]) #this is not the same as tracksWithSelectedItems. It gets more ids than the initial ones here. A content-linked note outside a selected track will still be affected. We start with the trackIds we already know.
listOfChangedTrackIds=set()
finalResult=[selectionValid,topLeft,bottomRight,listOfChangedTrackIds]#listOfChangedTrackIds is mutable. It will change in the loop below
track.goToTickindex(startTick,skipOverZeroDurations=False)#includes track.head() which resets the state
whiletrack.state.tickindex<=endTick:#we must use the tickindex here because we do not know the end items of the tracks
r=track.right()
ifr==1:
# the item we want is already left of the cursor. So we want the previous item.
iftrack.state.tickindex<=endTick:#yes, it is correct that the state in the parameter is ahead by one position. Why is it that way? Because everything that matters, like new dynamics will only be parsed afterwards. The trackState is always correct except for the tickindex when exporting after parsing. Thats why exportObject sometimes substracts its own duration for finding its starting tick.
listOfChangedTrackIds.add(id(parentBlock.parentTrack))#there are items without parent Tracks. They have None. We must filter them out otherwise our changedTrackIds get polluted with id(None)
#self.copyObjectsBuffer = [] #maybe that is a bit harsh. If you press Cltr+C by accident, without a selection, you loose your copied data. This is especially bad for cut and paste
#TODO: check if the cursor is still in the correct position after delete selection
listOfChangedTrackIds=set()
fornumber,trackinenumerate(workBuffer):
curTrack=self.currentTrack()
#Here comes an additional condition. Most of the time pastes are single-track. If so, or we simply are in the starting track, we want to paste at the actual position, not the tickindex. This circumvents problems with block boundaries and zero-duration items.
#TODO: In the future add a "smart paste" that checks if the cursor starts at the beginning or at the end of zero duration items or a block boundary and try to match that position in the other tracks, eventhough we can't be certain if these conditions actually match
ifself.trackIndex==trackIndexStart:
self.goTo(*startPosition)#has assertions as test within
else:
curTrack.goToTickindex(tickStart)
#assert curTrack.state.tickindex == tickStart #does not work with overwrite selection
foritemintrack:
newItem=item.copy()
curTrack.insert(newItem)#eventhough copyObjectsBuffer is already a copy of the original notes we don't want every paste to be the same instances.
#newItem has now a parentBlock and a parentTrack. We add this parent track to the list of changed Tracks
listOfChangedTrackIds.update(newItem.parentTrackIds)#TODO: profiling. All items in a block have the same parentTrack. This is highly redundant BUT can be different for items which originated from block boundaries
#Return to the item where pasting starting. Pasting counts as insert, and insert sticks to the item right of cursor.
#Therefore we go to the starting track and block, but not to the starting localCursorIndexInBlock. Instead we search for the specific item in a second step
self.goToItemInCurrentBlock(startItem)#we actually want to be at the same item where we started, not just the tick index which gets confused with block boundaries and zero duration items
#We need to find the topLeft selected item. This is NOT goToSelectionStart() which would be the actual starting point of the user, which includes bottomRight.
#go to first selected track, starting tick position. selections are always top left to bottom right
#else: yet unknown set of scenarios. One known is that the selection was from right to left, included normal duration items but ends on a block split which is at the tracks beginning. most likely a leftover from some deletion.
#NO! bottomRight is not the last selected item but the first item that is not selected anymore!! assert bottomRightCursor["item"] is lastSelectedItem, (bottomRightCursor["item"], lastSelectedItem)
iffirstRound:#In the first track the user selected a specific item, which may be zero duration or not. We must delete this, not more not less. All other tracks are approximations and can work with the tickindex.
self.currentTrack().toItemInBlock(topLeftCursor["item"],topLeftCursor["block"])#includes track.head() which resets the state
#We delete always from the same cursor position. After one deletion the rest items gravitate left to our position. Except block boundaries, those will not be crossed so we have to check for them:
break#without this the program will delete the first non-selected item, if zero duration, as well
endOfTrackBreaker=False
whilecurTrack.currentBlock().isAppending():#Block end, or even track end
ifnotcurTrack.right():#end of track.
endOfTrackBreaker=True
break
ifendOfTrackBreaker:
break
assertcurTrack.currentItem()isitem,(curTrack.currentItem(),item)#since listOfSelectedItems already removed all duplicate items this will work.
curTrack.delete()# side effect: delete the actual item. returns none on block boundary.
#Don't go down if we are already in the last track of the selection
ifnotnumber+1==numberOfTracks:#enumerate from 0, len from 1.
self.trackDown()
#assert curTrack.state.tickindex == topLeftCursor["tickindex"] We cannot be sure of this. The selection could have started at a content-linked item that got deleted and the tickindex has no item anymore
#return [id(track) for track in self.tracks] #TODO: only for debug reasons. the GUI will draw everything with this! Either this function or the api has to figure out the tracks that really changed. we could do a len check of the blocks data.
assertlen(self.trackById(trackId).blocks)==len(listWithBlocksData)#paste and delete selection never delete or create blocks. So the len is always the same. we can zip
return[id(track)fortrackinself.tracks]#TODO: only for debug reasons. the GUI will draw everything with this! Either this function or the api has to figure out the tracks that really changed. we could do a len check of the blocks data.
deflistOfTrackIds(self):
"""Mostly used for the initial callback sweep after loading
anewfileintheapi."""
#return Track.allTracks.keys() #this does contain deleted tracks (in the undo stack) as well. It is only valid when the program starts up. The undo stack is empty then.
#assert len (Track.allTracks) == len(self.tracks) #make sure to only call this function when it is correct. If we ever use this for non-startup reasons change to the list comprehension below.
#return Track.allTracks.keys()
return[id(track)fortrackinself.tracks]#tracks in the score, not in the undo stack.
deflistOfStaticTrackRepresentations(self):
"""A list of tracks"""
result=[]
fortrackinself.tracks:
static=track.staticTrackRepresentation()
static["hiddenPosition"]=None
result.append(static)
returnresult
deflistOfStaticHiddenTrackRepresentations(self):
"""A list of hidden tracks. Mutually exclusive with
#We don't need to actually delete the blocks here, this will be done indirectly by rearrangeBlocks through the api later.
blockOrder.remove(id(secondBlock))
assertnotid(secondBlock)inblockOrder
dictOfTrackIdsWithListOfBlockIds[parentTrackId]=blockOrder#eventhough it is mutable and therefore changes in the list are reflected in the dict-value we did not make sure that the list is in the dict in the first place above.
protoBlock.linkedContentBlocks.remove(protoBlock)
del(protoBlock)
returndictOfTrackIdsWithListOfBlockIds
defsplitCurrentBlockAndAllContentLinks(self):
"""split the current block around the cursor position.
protoBlockLeft.data=startBlock.data[:startBlock.localCursorIndex]#everything left of the cursor. The local cursor index of the current block is guaranteed to be correct. The one from other blocks is not.
protoBlockRight=startBlock.copy(track)
protoBlockRight.data=startBlock.data[startBlock.localCursorIndex:]#everything right of the cursor. The local cursor index of the current block is guaranteed to be correct. The one from other blocks is not.
forblockinstartBlock.linkedContentBlocksInScore():#all linked blocks in all tracks, including currentBlock. All blocks are unique. The content is not.
blockOrder[positionToInsert]=newLeftId#replaces the original block which will be kept intact for undo
blockOrder.insert(positionToInsert+1,newRightId)
dictOfTrackIdsWithListOfBlockIds[parentTrackId]=blockOrder#eventhough it is mutable and therefore changes in the list are reflected in the dict-value we did not make sure that the list is in the dict in the first place above.
protoBlockLeft.linkedContentBlocks.remove(protoBlockLeft)#probably redundant. But makes the intention clear.