Browse Source

various changes. tuplets mostly, but also drag and drop of blocks

master
Nils 4 years ago
parent
commit
52b9b99007
  1. 18
      engine/api.py
  2. 10
      engine/block.py
  3. 161
      engine/items.py
  4. 73
      engine/main.py
  5. 12
      engine/track.py
  6. 16
      qtgui/items.py
  7. 4
      qtgui/mainwindow.py
  8. 64
      qtgui/musicstructures.py
  9. 20
      qtgui/scorescene.py

18
engine/api.py

@ -28,6 +28,7 @@ random.seed()
from typing import Iterable, Callable, Tuple
#Third Party Modules
from calfbox import cbox
#Template Modules
import template.engine.api #we need direct access to the module to inject data in the provided structures. but we also need the functions directly. next line:
@ -309,7 +310,6 @@ def startEngine(nsmClient):
#General and abstract Commands
def getMetadata():
return session.data.metaData
@ -1324,6 +1324,8 @@ def _applyToSelection(itemFunctionAsString, parameterListForEachSelectedItem = [
Or, a typical case, just one parameter, the same, for each item in
the selection. The parameter itself can be a list itself as well.
The Following example is outdated. Tuplets are no longer a list but just a single tuple, no nesting. We leave this here as example only!
For example a duration.tuplets, which is list of tuples(!)
theParameter = [[(2,3)]] #This is a tuplet with only one fraction, triplet. It is for a notelist with only one note.
#theParameter = [ [(2,3), (4,5)], [(4,5), (1,2), (3,4)] ] #notelist of 2 with a double-nested tuplet for the first and a triple nested tuplet for the second note.
@ -2506,6 +2508,20 @@ def getLilypondRepeatList()->Iterable[Tuple[str, Callable]]:
("Close", lambda: lilypondText('\\bar ":|."')),
]
#This and That
def connectModMidiMerger():
"""Connect our global step midi in to mod-midi-merger, if present"""
jackClientName = cbox.JackIO.status().client_name
try:
cbox.JackIO.port_connect("mod-midi-merger:out", jackClientName + ":in")
except:
pass
#Debug
def printPitches():
track = session.data.currentTrack()

10
engine/block.py

@ -331,7 +331,11 @@ class Block(object):
else:
return False
def lilypond(self):
"""Called by track.lilypond(), returns a string"""
return " ".join(item.lilypond() for item in self.data)
def lilypond(self, carryLilypondRanges):
"""Called by track.lilypond(), returns a string.
carryLilypondRanges is handed from item to item for ranges
such as tuplets.
Can act like a stack or simply remember stuff.
"""
return " ".join(item.lilypond(carryLilypondRanges) for item in self.data)

161
engine/items.py

@ -184,7 +184,7 @@ class Note(object):
"accidental" : self.accidental(trackState.keySignature()),
"dotOnLine" : self.asDotOnLine(trackState.clef()),
"dots" : self.duration.dots,
"tuplets" : self.duration.tuplets,
"tuplet" : self.duration.tuplet,
"notehead" : self.duration.notehead,
"completeDuration" : dur,
@ -220,11 +220,11 @@ class Note(object):
return result
def lilypond(self):
"""Called by chord.lilypond(), See Item.lilypond for the general docstring.
def lilypond(self, carryLilypondRanges):
"""Called by chord.lilypond(carryLilypondRanges), See Item.lilypond for the general docstring.
Returns two strings, pitch and duration.
The duration is per-chord or needs special polyphony."""
return pitchmath.pitch2ly[self.pitch], self.duration.lilypond()
return pitchmath.pitch2ly[self.pitch], self.duration.lilypond(carryLilypondRanges)
class Dynamic(object):
"""dynamic means velocity midi terms.
@ -319,7 +319,8 @@ class Duration(object):
def __init__(self, baseDuration):
self._baseDuration = baseDuration #base value. Without dots, tuplets/times , overrides etc. this is the duration for all representations
self.tuplets = [] # a list of tuplets [(numerator, denominator), (,)...]. normal triplet is (2,3). Numerator is the level of the notehead. 2^n Each is one level of tuplets, arbitrary nesting depth. [2,3] for triplet # Tuplet multiplication is used for all representations and gets auto-merged on lilypond output to tuplet groups (if its in a chord). This is a list and not tuple() because json load will make it a list anyway.
#DEPRECTATED. WILL BREAK SAVE FILES. self.tuplets = [] # a list of tuplets [(numerator, denominator), (,)...]. normal triplet is (2,3). Numerator is the level of the notehead. 2^n Each is one level of tuplets, arbitrary nesting depth. [2,3] for triplet # Tuplet multiplication is used for all representations and gets auto-merged on lilypond output to tuplet groups (if its in a chord). This is a list and not tuple() because json load will make it a list anyway.
self.tuplet = None #a single tuple (numerator, denominator). There is no nesting in Laborejo. Numerator is the level of the notehead. 2^n Each is one level of tuplets, arbitrary nesting depth. [2,3] for triplet # Tuplet multiplication is used for all representations and gets auto-merged on lilypond output to tuplet groups (if its in a chord).
self.dots = 0 #number of dots.
self.durationKeyword = D_DEFAULT
#The offsets shift the start and ending to the left and right or rather: earlier and later. positive values mean later, negative values mean earlier.
@ -383,12 +384,12 @@ class Duration(object):
new.dots = 2
elif completeDuration *3/2 in durList: #triplets are so a common we take care of them instead of brute forcing with fractions in the else branch
new = cls(guessedBase)
new.tuplets = [(2,3)]
new.tuplet = (2,3)
else: #tuplet. That means the value is below a standard "notehead" duration. We need to find how much much lower.
new = cls(guessedBase)
#ratio = completeDuration / guessedBase #0.666~ for triplet
newRatio = Fraction(int(completeDuration), guessedBase).limit_denominator(100000) #protects 6 or 7 decimal positions
new.tuplets = [(newRatio.numerator, newRatio.denominator)]
new.tuplet = (newRatio.numerator, newRatio.denominator)
else:
#the base could not be guessed. In this case we just create a non-standard note.
warn("non-standard duration generated")
@ -402,7 +403,16 @@ class Duration(object):
assert cls.__name__ == serializedObject["class"]
self = cls.__new__(cls)
self._baseDuration = int(serializedObject["baseDuration"])
self.tuplets = serializedObject["tuplets"] #TODO: make sure the types are correct?
if "tuplets" in serializedObject and serializedObject["tuplets"]:
#old file format, which allowed nested tuplets. Was never used by anyone.
#convert to new single-tuplet format and this will be gone next save.
self.tuplet = serializedObject["tuplets"][0] #just take the first element. There was never a nested tuple, the gui never supported it.
elif "tuplet" in serializedObject and serializedObject["tuplet"]:
#Current file format.
self.tuplet = tuple(serializedObject["tuplet"])
else:
self.tuplet = None
self.dots = int(serializedObject["dots"])
self.durationKeyword = int(serializedObject["durationKeyword"])
self.shiftStart = int(serializedObject["shiftStart"])
@ -414,7 +424,7 @@ class Duration(object):
result = {}
result["class"] = "Duration"
result["baseDuration"] = self._baseDuration
result["tuplets"] = self.tuplets
result["tuplet"] = self.tuplet
result["dots"] = self.dots
result["shiftStart"] = self.shiftStart
result["shiftEnd"] = self.shiftEnd
@ -436,7 +446,7 @@ class Duration(object):
def copy(self):
new = Duration(self.baseDuration)
new.tuplets = self.tuplets.copy()
new.tuplet = self.tuplet
new.dots = self.dots
new.genericNumber = new.calcGenericNumber()
new.notehead = new.calcNotehead()
@ -481,7 +491,8 @@ class Duration(object):
return self.cachedCompleteDuration
else:
value = 2 * self.baseDuration - self.baseDuration / 2**self.dots
for numerator, denominator in self.tuplets:
if self.tuplet:
numerator, denominator = self.tuplet
value = value * numerator / denominator
if not value == int(value):
@ -563,13 +574,13 @@ class Duration(object):
self.dots = 0
def toggleTriplet(self):
if self.tuplets == [(2,3)]:
self.tuplets = []
if self.tuplet == (2,3):
self.tuplet = None
else:
self.tuplets = [(2,3)]
self.tuplet = (2,3)
def lilypond(self):
"""Called by note.lilypond(), See Item.lilypond for the general docstring.
def lilypond(self, carryLilypondRanges):
"""Called by note.lilypond(carryLilypondRanges), See Item.lilypond for the general docstring.
returns a number as string."""
if self.durationKeyword == D_TIE:
@ -620,7 +631,7 @@ class DurationGroup(object):
def hasTuplet(self):
"""Return true if there is any tuplet in any note"""
return any(note.duration.tuplets for note in self.chord.notelist)
return any(note.duration.tuplet for note in self.chord.notelist)
@property
def baseDuration(self):
@ -841,14 +852,14 @@ class Item(object):
def mirrorAroundCursor(self, cursorPitch): pass
def lilypond(self):
def lilypond(self, carryLilypondRanges):
if self.lilypondParameters["override"]:
return self.lilypondParameters["override"]
else:
return self._lilypond()
return self._lilypond(carryLilypondRanges)
def _lilypond(self):
"""called by block.lilypond(), returns a string.
def _lilypond(self, carryLilypondRanges):
"""called by block.lilypond(carryLilypondRanges), returns a string.
Don't create white-spaces yourself, this is done by the structures.
When in doubt prefer functionality and robustness over 'beautiful' lilypond syntax."""
return ""
@ -919,8 +930,8 @@ class TemplateItem(Item):
"UIstring" : "", #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible.
}
def _lilypond(self):
"""called by block.lilypond(), returns a string.
def _lilypond(self, carryLilypondRanges):
"""called by block.lilypond(carryLilypondRanges), returns a string.
Don't create white-spaces yourself, this is done by the structures.
When in doubt prefer functionality and robustness over 'beautiful' lilypond syntax."""
return ""
@ -1169,10 +1180,10 @@ class Chord(Item):
self.durationGroup.cacheMinimumNote()
return lambda: self._setNoteDuration(note, oldValueForUndo)
def setTupletNearPitch(self, pitch, tupletListForDuration):
def setTupletNearPitch(self, pitch, tupletForDuration):
note = self.getNearestNote(pitch)
oldValueForUndo = note.duration.copy()
note.duration.tuplets = tupletListForDuration
note.duration.tuplet = tupletForDuration
self.durationGroup.cacheMinimumNote()
return lambda: self._setNoteDuration(note, oldValueForUndo)
@ -1425,20 +1436,20 @@ class Chord(Item):
self.durationGroup.cacheMinimumNote()
return lambda: self._setDurationlist(oldValues)
def setTuplet(self, durationTupletListForEachNote):
def setTuplet(self, durationTupletForEachNote:list):
"""
parameter format:
[
[(2,3)], #note 1
[(4,5), (2,3)], #note 2
[(3,4)], #note 3
(2,3), #note 1
(4,5), #note 2
(3,4), #note 3
]
"""
gen = self._createParameterGenerator(self.notelist, durationTupletListForEachNote)
gen = self._createParameterGenerator(self.notelist, durationTupletForEachNote)
oldValues = []
for note in self.notelist:
oldValues.append(note.duration.copy()) #see _setDurationlist
note.duration.tuplets = next(gen)
note.duration.tuplet = next(gen)
self.durationGroup.cacheMinimumNote()
return lambda: self._setDurationlist(oldValues)
@ -1697,13 +1708,43 @@ class Chord(Item):
"beam" : tuple(), #decided later in track export. Has the same structure as a stem.
}
def _lilypond(self):
"""Called by block.lilypond(), returns a string.
def _lilypond(self, carryLilypondRanges):
"""Called by block.lilypond(carryLilypondRanges), returns a string.
See Item.lilypond for the general docstring."""
pitches, durations = zip(*(note.lilypond() for note in self.notelist))
def _createLilypondTuplet(duration, carryLilypondRanges:dict)->str:
pre = ""
# \tuplet 3/2 { }
if duration.tuplet: #item has a tuplet...
if "tuplet" in carryLilypondRanges: #... and is part of a group (2nd or more)
if duration.tuplet == carryLilypondRanges["tuplet"]:
pass #this is nothing and the entire reason why we check for group membership. Tuplet-members in ly are just inside a {...} music section.
else: #it just happened that two different tuplet types followed next to each other. end one, start a new tuplet group.
carryLilypondRanges["tuplet"] = duration.tuplet
pre = f" }} \\tuplet {duration.tuplet[1]}/{duration.tuplet[0]} {{ "
else: # ... and is the first of a group
carryLilypondRanges["tuplet"] = duration.tuplet
pre = f"\\tuplet {duration.tuplet[1]}/{duration.tuplet[0]} {{ "
else:
if "tuplet" in carryLilypondRanges: #... and the previous item was a tuplet. Group has ended.
pre = " } " #hopefully this just ends the tuplet :) . Pre is correct, not post. Because we end the tuplet of the previous item.
del carryLilypondRanges["tuplet"]
else: #... and the previous item was also not a tuplet. Most common case.
pass
return pre
#Create lilypond durations
pitches, durations = zip(*(note.lilypond(carryLilypondRanges) for note in self.notelist))
onlyOneDuration = durations.count(durations[0]) == len(durations)
if onlyOneDuration:
return "<" + " ".join(pitches) + ">" + durations[0]
pre = _createLilypondTuplet(self.notelist[0].duration, carryLilypondRanges)
if len(pitches) == 1:
# The most common case: Just a single note
return pre + pitches[0] + durations[0] #this is a string.
else:
# A simple chord with mulitple pitches but just one duration
return pre + "<" + " ".join(pitches) + ">" + durations[0]
else:
#TODO: Cache this
@ -1713,7 +1754,7 @@ class Chord(Item):
#See http://lilypond.org/doc/v2.18/Documentation/notation/writing-rhythms#scaling-durations
table = {}
for note in self.notelist:
pitch, duration = note.lilypond()
pitch, duration = note.lilypond(carryLilypondRanges)
ticks = note.duration.completeDuration()
if not (ticks, duration) in table:
table[(ticks, duration)] = list()
@ -1802,9 +1843,9 @@ class Rest(Item):
self.duration.toggleTriplet()
return lambda: self._setDuration(oldValue)
def setTupletNearPitch(self, pitch, tupletListForDuration):
def setTupletNearPitch(self, pitch, tuplet):
oldValue = self.duration.copy()
self.duration.tuplets = tupletListForDuration
self.duration.tuplet = tuplet
return lambda: self._setDuration(oldValue)
#Apply to selection gets called by different methods. We can just create aliases for Rest
@ -1813,17 +1854,17 @@ class Rest(Item):
toggleDot = toggleDotNearPitch
toggleTriplet = toggleTripletNearPitch
def setTuplet(self, durationTupletListForEachNote):
def setTuplet(self, durationTupletForEachNote:list):
"""
parameter format compatible with Chord
[
[(2,3)], #note 1
[(4,5), (2,3)], #note 2
[(3,4)], #note 3
(2,3), #note 1
(4,5), #note 2
(3,4), #note 3
]
"""
oldValue = self.duration.copy()
self.duration.tuplets = durationTupletListForEachNote[0]
self.duration.tuplet = durationTupletForEachNote[0] #TODO: ? reasoning to only take the first? I think this is right, I just forgot why.
return lambda: self._setDuration(oldValue)
def calcFlag(self):
@ -1842,7 +1883,7 @@ class Rest(Item):
"baseDuration": self.duration.baseDuration,
"completeDuration" : dur,
"dots" : self.duration.dots,
"tuplets" : self.duration.tuplets,
"tuplet" : self.duration.tuplet,
"tickindex" : trackState.tickindex - dur, #because we parse the tickindex after we stepped over the item.
"midiBytes" : [],
#for compatibility with beaming groups:
@ -1853,13 +1894,13 @@ class Rest(Item):
}
def _lilypond(self):
"""Called by block.lilypond(), returns a string.
def _lilypond(self, carryLilypondRanges):
"""Called by block.lilypond(carryLilypondRanges), returns a string.
See Item.lilypond for the general docstring."""
if self.lilypondParameters["visible"]:
return "r{}".format(self.duration.lilypond())
return "r{}".format(self.duration.lilypond(carryLilypondRanges))
else:
return "s{}".format(self.duration.lilypond())
return "s{}".format(self.duration.lilypond(carryLilypondRanges))
class MultiMeasureRest(Item):
def __init__(self, numberOfMeasures):
@ -1960,8 +2001,8 @@ class MultiMeasureRest(Item):
self.numberOfMeasures = self.numberOfMeasures / 2
return lambda: self._setNumberOfMeasures(oldValue)
def _lilypond(self):
"""Called by block.lilypond(), returns a string.
def _lilypond(self, carryLilypondRanges):
"""Called by block.lilypond(carryLilypondRanges), returns a string.
See Item.lilypond for the general docstring."""
lyduration = "{}/{}".format(self._cachedMetricalInstruction.nominator, duration.baseDurationToTraditionalNumber[self._cachedMetricalInstruction.denominator])
@ -2154,7 +2195,7 @@ class KeySignature(Item):
}
def _lilypond(self):
def _lilypond(self, carryLilypondRanges):
def build(step, alteration): #generate a Scheme pair for Lilyponds GUILE interpreter
if alteration == -10:
return "(" + str(step) + " . ," + "FLAT)"
@ -2292,7 +2333,7 @@ class Clef(Item):
#Thats it. A GUI does not need to know anything about the clef except its look because we already deliever note pitches as dots on lines, calculated with the clef.
}
def _lilypond(self):
def _lilypond(self, carryLilypondRanges):
return "\\clef \"{}\"".format(self.clefString)
class TimeSignature(Item): #Deprecated since 1750
@ -2415,7 +2456,7 @@ class MetricalInstruction(Item):
"treeOfInstructions" : self.treeOfInstructions.__repr__(),
}
def _lilypond(self):
def _lilypond(self, carryLilypondRanges):
"""Since metrical instruction can get very complex we rely entirely on the lilypond override
functionality. The common api signatures already include these."""
raise RuntimeError("This metrical instruction should have a lilypond-override")
@ -2706,8 +2747,8 @@ class InstrumentChange(Item):
"UIstring" : "{}[pr{}{}{}]".format(_nameStr, self.program, _msbStr, _lsbStr), #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible.
}
def _lilypond(self):
"""called by block.lilypond(), returns a string.
def _lilypond(self, carryLilypondRanges):
"""called by block.lilypond(carryLilypondRanges), returns a string.
Don't create white-spaces yourself, this is done by the structures.
When in doubt prefer functionality and robustness over 'beautiful' lilypond syntax."""
if self.shortInstrumentName:
@ -2768,8 +2809,8 @@ class ChannelChange(Item):
"UIstring" : f"{self.text}(ch{self.value+1})" if self.text else f"ch{self.value+1}", #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible.
}
def _lilypond(self):
"""called by block.lilypond(), returns a string.
def _lilypond(self, carryLilypondRanges):
"""called by block.lilypond(carryLilypondRanges), returns a string.
Don't create white-spaces yourself, this is done by the structures.
When in doubt prefer functionality and robustness over 'beautiful' lilypond syntax."""
if self.text:
@ -2848,7 +2889,7 @@ class RecordedNote(Item):
"UIstring" : "", #this is for a UI, possibly a text UI, maybe for simple items of a GUI. Make it as short and unambigious as possible.
}
def _lilypond(self):
def _lilypond(self, carryLilypondRanges):
"""absolutely not"""
return ""
@ -2864,8 +2905,8 @@ class LilypondText(Item):
"""see Item._secondInit"""
super()._secondInit(parentBlock) #Item._secondInit
def _lilypond(self):
"""called by block.lilypond(), returns a string.
def _lilypond(self, carryLilypondRanges):
"""called by block.lilypond(carryLilypondRanges), returns a string.
Don't create white-spaces yourself, this is done by the structures.
When in doubt prefer functionality and robustness over 'beautiful' lilypond syntax."""
return self.text

73
engine/main.py

@ -39,14 +39,14 @@ from .lilypond import fromTemplate
class Data(template.engine.sequencer.Score):
def __init__(self, parentSession):
super().__init__(parentSession)
super().__init__(parentSession)
self.tracks = [Track(parentData = self)] #this is the track order and all visible tracks. For getting a specific track use Track.allTracks with a track id.
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.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
#Metadata has only strings as keys, even the numbers.
self.metaData = {key:"" for key in ("title", "subtitle", "dedication","composer","subsubtitle","instrument","meter","arranger", "poet","piece","opus","copyright","tagline", "subtext")}
self.currentMetronomeTrack = self.tracks[0] #A Laborejo Track, indepedent of currentTrack. The metronome is in self.metronome, set by the template Score.
self.metaData = {key:"" for key in ("title", "subtitle", "dedication","composer","subsubtitle","instrument","meter","arranger", "poet","piece","opus","copyright","tagline", "subtext")}
self.currentMetronomeTrack = self.tracks[0] #A Laborejo Track, indepedent of currentTrack. The metronome is in self.metronome, set by the template Score.
self._processAfterInit()
def _processAfterInit(self):
@ -56,12 +56,12 @@ class Data(template.engine.sequencer.Score):
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)
def duration(self):
"""Return the duration of the whole score, in ticks"""
#TODO: use cached duration? How often is this used? Pretty often. 3 Times for a single track note update.
#TODO: Measure before trying to improve performance.
#TODO: Measure before trying to improve performance.
result = []
for track in self.tracks:
result.append(track.duration())
@ -78,7 +78,7 @@ class Data(template.engine.sequencer.Score):
except:
print ("Error while trying to get track by id")
print (sys.exc_info()[0])
print (trackId, hex(trackId), Track.allTracks, self.tracks, self.hiddenTracks, self.trackIndex)
print (trackId, hex(trackId), Track.allTracks, self.tracks, [(track.name, id(track)) for track in self.tracks], self.hiddenTracks, self.trackIndex)
raise
return ret
@ -131,9 +131,9 @@ class Data(template.engine.sequencer.Score):
atIndex inserts the track before the item currently
in this position. If atIndex is bigger than the highest current
index it will be appended through standard python behaviour
We don't use the template-function addTrack.
"""
We don't use the template-function addTrack.
"""
assert trackObject.sequencerInterface
assert trackObject.parentData == self
self.tracks.insert(atIndex, trackObject)
@ -180,11 +180,11 @@ class Data(template.engine.sequencer.Score):
if len(self.tracks) > 1:
nowInTicks = self.currentTrack().state.tickindex
result = self.currentTrack(), self.trackIndex #return to the API for undo.
super().deleteTrack(self.currentTrack())
super().deleteTrack(self.currentTrack())
#self.currentTrack().prepareForDeletion() #remove calfbox objects
#self.tracks.remove(self.currentTrack())
if self.trackIndex+1 > len(self.tracks): #trackIndex from 0, len from 1.
self.trackIndex -= 1
self.currentTrack().head()
@ -436,8 +436,8 @@ class Data(template.engine.sequencer.Score):
tickindex and position.
We take care of duration-changed content linked items left of the selection as well.
"""
t = self.selectionExport() #is there a selection at all?
t = self.selectionExport() #is there a selection at all?
if not t:
return [False, None, None, [], []] #mimickes the true result. the api can use this to decide to do nothing instead of processing the selection
else:
@ -451,7 +451,7 @@ class Data(template.engine.sequencer.Score):
#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
firstRound = True
for track in tracksWithSelectedItems:
assert track in self.tracks, track #selecting from hidden or deleted tracks is impossible
@ -469,7 +469,7 @@ class Data(template.engine.sequencer.Score):
if r == 1:
# the item we want is already left of the cursor. So we want the previous item.
if track.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.
item = track.previousItem()
item = track.previousItem()
if (not removeContentLinkedData) or (not item in seenItems):
result.append((item, {"keySignature" : track.state.keySignature()})) #The keysig is needed for undo operations after apply to selection
seenItems.add(item)
@ -487,10 +487,11 @@ class Data(template.engine.sequencer.Score):
track.toPosition(originalPosition) #has head() in it
finalResult.append(result)
for changedTrId in listOfChangedTrackIds:
#hier weiter machen. Die ids gehen durcheinander. Es ist nicht nur der assert check. Wenn wir das weglassen kommt das Problem später
for changedTrId in listOfChangedTrackIds: #It is not the track ids that change but tracks changed and these are their IDs:
assert self.trackById(changedTrId) in self.tracks, changedTrId #selecting from hidden or deleted tracks is impossible
return finalResult
#The next two functions have "Objects" in their name so copy gets not confused with block.copy or item.copy. This is Ctrl+C as in copy and paste.
@ -514,7 +515,7 @@ class Data(template.engine.sequencer.Score):
result = []
if validSelection:
for track in selectedTracksAndItems:
result.append([item.copy() for item, cachedTrackState in track])
result.append([item.copy() for item, cachedTrackState in track])
if writeInSessionBuffer:
self.copyObjectsBuffer = result
return result
@ -618,10 +619,10 @@ class Data(template.engine.sequencer.Score):
self.trackUp()
assert self.trackIndex == trackIndexStart == startPosition[0]
assert self.currentTrack().state.tickindex == finalTickIndex #this is not enough to pinpoint the real location.
#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.goTo(trackIndex=startPosition[0], blockindex=startPosition[1], localCursorIndexInBlock=0)
self.goTo(trackIndex=startPosition[0], blockindex=startPosition[1], localCursorIndexInBlock=0)
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
try: #overwrite? #TODO: What is that? assert with AssertionError pass?
@ -637,7 +638,7 @@ class Data(template.engine.sequencer.Score):
"""Mortals, Hear The Prophecy: Delete Selection is notoriously tricky. It was hard in Laborejo 1 and it is still hard
here. Please use as many asserts and tests as you possibly can. The next bug or regression
WILL come. And it will be here.
Will only delete from visible tracks. No hidden, no deleted.
"""
@ -707,7 +708,7 @@ class Data(template.engine.sequencer.Score):
#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.
return listOfChangedTrackIds
def getBlockAndItemOrder(self):
@ -852,7 +853,7 @@ class Data(template.engine.sequencer.Score):
#what = Track.allTracks.values() #this includes deleted tracks
else:
what = self.tracks
for track in what:
for block in track.blocks:
for item in block.data:
@ -968,7 +969,7 @@ class Data(template.engine.sequencer.Score):
startBlock = track.currentBlock() #this is the only unique thing we can rely on.
#startBlock will get deleted, but we don't remove the blocks as parent because we need them for undo
#They will be finally cleared on save.
#They will be finally cleared on save.
#NO. This works for splitting, but not for undo. for item in startBlock.data:
# item.parentBlocks.remove(startBlock)
@ -1019,17 +1020,17 @@ class Data(template.engine.sequencer.Score):
def removeEmptyBlocks(self):
dictOfTrackIdsWithListOfBlockIds = {} # [trackId] = [listOfBlockIds]
#for trId, track in Track.allTracks.items():
for track in list(self.hiddenTracks.keys()) + self.tracks:
if len(track.blocks) <= 1:
continue #next track
#Get all blocks and then remove those with no content.
trId = id(track)
listOfBlockIds = track.asListOfBlockIds()
listOfBlockIds = track.asListOfBlockIds()
for block in track.blocks:
if not block.data:
if not block.data:
listOfBlockIds.remove(id(block))
#Maybe the track was empty. In this case we add one of the blocks again. Otherwise follow up functions will not act.
if not listOfBlockIds:
@ -1051,14 +1052,14 @@ class Data(template.engine.sequencer.Score):
#Save / Load / Export
def serialize(self)->dict:
dictionary = super().serialize()
dictionary = super().serialize()
dictionary["class"] = self.__class__.__name__
dictionary["tracks"] = [track.serialize() for track in self.tracks]
#we can't save hiddenTracks as dict because the serialized track is a dict itself, which is not hashable and therefore not a dict-key.
dictionary["hiddenTracks"] = [[track.serialize(), originalIndex] for track, originalIndex in self.hiddenTracks.items()]
dictionary["tempoTrack"] = self.tempoTrack.serialize()
dictionary["metaData"] = self.metaData
dictionary["currentMetronomeTrackIndex"] = self.tracks.index(self.currentMetronomeTrack)
dictionary["currentMetronomeTrackIndex"] = self.tracks.index(self.currentMetronomeTrack)
return dictionary
@classmethod
@ -1066,17 +1067,17 @@ class Data(template.engine.sequencer.Score):
assert cls.__name__ == serializedData["class"]
self = cls.__new__(cls)
super().copyFromSerializedData(parentSession, serializedData, self) #Tracks, parentSession and tempoMap
self.parentSession = parentSession
self.parentSession = parentSession
self.hiddenTracks = {Track.instanceFromSerializedData(parentData=self, serializedData=track):originalIndex for track, originalIndex in serializedData["hiddenTracks"]}
self.tempoTrack = TempoTrack.instanceFromSerializedData(serializedData["tempoTrack"], parentData = self)
self.metaData = serializedData["metaData"]
self.currentMetronomeTrack = self.tracks[serializedData["currentMetronomeTrackIndex"]]
self.currentMetronomeTrack = self.tracks[serializedData["currentMetronomeTrackIndex"]]
self._processAfterInit()
return self
def export(self)->dict:
dictionary = super().export()
return {
"numberOfTracks" : len(self.tracks),
"howManyUnits" : self.howManyUnits,

12
engine/track.py

@ -815,7 +815,13 @@ class Track(object):
#Save / Load / Export
def lilypond(self):
"""Called by score.lilypond(), returns a string."""
"""Called by score.lilypond(), returns a string.
We carry a dict around to hold lilypond on/off markers like tuplets
that need to set as ranges.
"""
for item in self.blocks[0].data[:4]:
if type(item) is MetricalInstruction:
timeSig = ""
@ -825,7 +831,9 @@ class Track(object):
upbeatLy = "\\partial {} ".format(Duration.createByGuessing(self.upbeatInTicks).lilypond()) if self.upbeatInTicks else ""
data = " ".join(block.lilypond() for block in self.blocks)
carryLilypondRanges = {} #handed from item to item for ranges such as tuplets. Can act like a stack or simply remember stuff.
data = " ".join(block.lilypond(carryLilypondRanges) for block in self.blocks)
if data:
return timeSig + upbeatLy + data
else:

16
qtgui/items.py

@ -280,14 +280,15 @@ class GuiNote(QtWidgets.QGraphicsItem):
d.setPos((dot+1)*4+6, constantsAndConfigs.stafflineGap * noteExportObject["dotOnLine"] / 2)
d.setParentItem(self)
for i, (upper, lower) in enumerate(noteExportObject["tuplets"]):
tuplet = GuiTupletNumber(upper, lower)
#Draw the tuplet. There are no nested tuplets in Laborejo.
if noteExportObject["tuplet"]:
upper, lower = noteExportObject["tuplet"]
tuplet = GuiTupletNumber(upper, lower)
if directionRightAndUpwards:
tuplet.setPos(3, -6*(i+1))
tuplet.setPos(3, -6)
else:
tuplet.setPos(3, 6*(i+1))
tuplet.setPos(3, 6)
tuplet.setParentItem(self.noteHead)
if noteExportObject["durationKeyword"]:
@ -607,10 +608,11 @@ class GuiRest(GuiItem):
d.setPos((dot+1)*4+6, -2)
d.setParentItem(self)
for i, (upper, lower) in enumerate(self.staticItem["tuplets"]):
if self.staticItem["tuplet"]:
upper, lower = self.staticItem["tuplet"]
tuplet = GuiTupletNumber(upper, lower)
tuplet.setParentItem(self)
tuplet.setPos(2, (i+1)*-4-constantsAndConfigs.stafflineGap*2)
tuplet.setPos(2, -4-constantsAndConfigs.stafflineGap*2)
class GuiKeySignature(GuiItem):
def __init__(self, staticItem):

4
qtgui/mainwindow.py

@ -128,6 +128,8 @@ class MainWindow(TemplateMainWindow):
#Here is the crowbar-method.
self.nsmClient.announceSaveStatus(isClean = True)
api.connectModMidiMerger()
def zoom(self, scaleFactor:float):
"""Scale factor is absolute"""
@ -144,7 +146,7 @@ class MainWindow(TemplateMainWindow):
except:
print (c)
if i:
ly = i.lilypond()
ly = i.lilypond(carryLilypondRanges = {})
if (not ly) or len(ly) > 13:
ly = ""
else:

64
qtgui/musicstructures.py

@ -47,21 +47,21 @@ class GuiBlockHandle(QtWidgets.QGraphicsRectItem):
"""A simplified version of a Block. Since we don't use blocks in the GUI, only in the backend
we still need them sometimes as macro strutures, where we don't care about the content.
This is the transparent Block handle that appears when the user uses the mouse to drag and drop
a block with shift + middle mouse button.
a block.
It is visible all the time though and can be clicked on. In opposite to the background color,
which is just a color and stays in place.
"""
def __init__(self, parent, staticExportItem, x, y, w, h):
super().__init__(x, y, w, h)
self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden
self.setFlag(QtWidgets.QGraphicsItem.ItemContainsChildrenInShape, True)
#self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden
#self.setFlag(QtWidgets.QGraphicsItem.ItemContainsChildrenInShape, True)
self.parent = parent #GuiTrack instance
self.color = None #inserted by the creating function in GuiTrack
self.color = None #QColor inserted by the creating function in GuiTrack. Used during dragging, then reset to transparent.
self.trans = QtGui.QColor("transparent")
self.setPen(self.trans)
self.setBrush(self.trans)
self.setOpacity(0.4) #slightly fuller than background
#self.setOpacity(0.4) #slightly fuller than background
self.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
self.setParentItem(parent)
self.setZValue(10) #This is the z value within GuiTrack
@ -79,20 +79,23 @@ class GuiBlockHandle(QtWidgets.QGraphicsRectItem):
self.idText.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
"""
if self.staticExportItem["completeDuration"] >= api.D1: #cosmetics
self.startLabel = QtWidgets.QGraphicsSimpleTextItem(self.staticExportItem["name"] + translate("musicstructures", " start"))
self.startLabel.setParentItem(self)
self.startLabel.setPos(0, constantsAndConfigs.stafflineGap)
self.startLabel.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
if self.staticExportItem["completeDuration"] >= 3 * api.D1: #cosmetics
self.startLabel = QtWidgets.QGraphicsSimpleTextItem(self.staticExportItem["name"])
self.endLabel = QtWidgets.QGraphicsSimpleTextItem(self.staticExportItem["name"] + translate("musicstructures", " end "))
self.endLabel.setParentItem(self)
self.endLabel.setPos(self.rect().width() - self.endLabel.boundingRect().width(), constantsAndConfigs.stafflineGap)
self.endLabel.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
else:
self.startLabel = QtWidgets.QGraphicsSimpleTextItem("")
self.endLabel = QtWidgets.QGraphicsSimpleTextItem("")
self.startLabel.setParentItem(self)
self.startLabel.setPos(0, constantsAndConfigs.stafflineGap)
self.startLabel.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
self.endLabel.setParentItem(self)
self.endLabel.setPos(self.rect().width() - self.endLabel.boundingRect().width(), constantsAndConfigs.stafflineGap)
self.endLabel.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
def stretchXCoordinates(self, factor):
"""Reposition the items on the X axis.
Call goes through all parents/children, starting from ScoreView._stretchXCoordinates.
@ -108,40 +111,23 @@ class GuiBlockHandle(QtWidgets.QGraphicsRectItem):
self.startLabel.hide()
self.endLabel.hide()
def mouseMoveEventCustom(self, event):
# All the positions below don't work. They work fine when dragging Tracks around but not this Item. I can't be bothered to figure out why.
#scenePos() results ins an item position that is translated down and right. The higher the x/y value the more the offset
#Instead we calculate our delta ourselves.
#self.setPos(self.mapToItem(self, event.scenePos()))
#self.setPos(self.mapFromScene(event.scenePos()))
#posGlobal = QtGui.QCursor.pos()
#posView = self.parent.parentScore.parentView.mapFromGlobal(posGlobal) #a widget
#posScene = self.parent.parentScore.parentView.mapToScene(posView)
#print (posGlobal, posView, posScene, event.scenePos())
"""
#Does not work with zooming.
if self.cursorPosOnMoveStart:
delta = QtGui.QCursor.pos() - self.cursorPosOnMoveStart
new = self.posBeforeMove + delta
self.setPos(new)
"""
if self.cursorPosOnMoveStart:
self.setPos(event.scenePos())
super().mouseMoveEvent(event)
def mousePressEventCustom(self, event):
"""Not a qt-override. This is called directly by GuiScore
if you click on a block with the right modifier keys (none)"""
self.posBeforeMove = self.pos()
self.cursorPosOnMoveStart = QtGui.QCursor.pos()
self.setBrush(self.color)
self.endLabel.hide()
super().mousePressEvent(event)
def mouseReleaseEventCustom(self, event):
"""Not a qt-override. This is called directly by GuiScore
if you click-release on a block"""
self.setBrush(self.trans)
self.setPos(self.posBeforeMove) #In case the block was moved to a position where no track is (below the tracks) we just reset the graphics.
self.posBeforeMove = None
self.cursorPosOnMoveStart = None
self.endLabel.show()
super().mouseReleaseEvent(event)
def contextMenuEvent(self, event):
@ -149,7 +135,7 @@ class GuiBlockHandle(QtWidgets.QGraphicsRectItem):
listOfLabelsAndFunctions = [
(translate("musicstructures", "edit properties"), lambda: BlockPropertiesEdit(self.scene().parentView.mainWindow, staticExportItem = self.staticExportItem)),
("separator", None),
#("split here", lambda: self.splitHere(event)),
#("split here", lambda: self.splitHere(event)), #Impossible because we can't see notes.
(translate("musicstructures", "duplicate"), lambda: api.duplicateBlock(self.staticExportItem["id"])),
(translate("musicstructures", "create content link"), lambda: api.duplicateContentLinkBlock(self.staticExportItem["id"])),
(translate("musicstructures", "unlink"), lambda: api.unlinkBlock(self.staticExportItem["id"])),
@ -294,7 +280,7 @@ class GuiTrack(QtWidgets.QGraphicsItem):
bgItem.setZValue(-10) #This is the z value within GuiTrack
bgItem.setEnabled(False)
transparentBlockHandle = GuiBlockHandle(self, block, 0, -2 * constantsAndConfigs.stafflineGap, block["completeDuration"] / constantsAndConfigs.ticksToPixelRatio, h + 4 * constantsAndConfigs.stafflineGap) #x, y, w, h
transparentBlockHandle = GuiBlockHandle(self, block, 0, -2 * constantsAndConfigs.stafflineGap, block["completeDuration"] / constantsAndConfigs.ticksToPixelRatio, h-constantsAndConfigs.stafflineGap) #x, y, w, h
transparentBlockHandle.color = color
self.transparentBlockHandles.append(transparentBlockHandle)
transparentBlockHandle.setPos(block["tickindex"] / constantsAndConfigs.ticksToPixelRatio, -2*constantsAndConfigs.stafflineGap)

20
qtgui/scorescene.py

@ -339,21 +339,21 @@ class GuiScore(QtWidgets.QGraphicsScene):
def mousePressEvent(self, event):
"""Pressing the mouse button is the first action of drag
and drop. We make the mouse cursor invisible so the user
can see where the point is going
When in blockmode pressing the middle button combined with either Alt or Shift moves tracks and blocks.
can see where the object is moving
"""
if event.button() == 4 and self.parentView.mode() in ("block", "cc"): # Middle Button
if event.button() == QtCore.Qt.LeftButton and self.parentView.mode() in ("block", "cc"):
modifiers = QtWidgets.QApplication.keyboardModifiers()
if modifiers == QtCore.Qt.ShiftModifier: #block move
#Block Move
if modifiers == QtCore.Qt.NoModifier:
block = self.blockAt(event.scenePos())
if block: #works for note blocks and conductor blocks
block.staticExportItem["guiPosStart"] = block.pos() #without shame we hijack the backend-dict.
self.duringBlockDragAndDrop = block
block.mousePressEventCustom(event)
elif modifiers == QtCore.Qt.AltModifier and self.parentView.mode() == "block": #track move
#Track Move
elif (modifiers == QtCore.Qt.AltModifier or modifiers == QtCore.Qt.ShiftModifier ) and self.parentView.mode() == "block":
track = self.trackAt(event.scenePos())
if track and not track is self.conductor:
self.parentView.setCursor(QtCore.Qt.BlankCursor)
@ -370,11 +370,13 @@ class GuiScore(QtWidgets.QGraphicsScene):
it will not get mouseRelease or mouseMove events. MousePress always works."""
if self.duringTrackDragAndDrop:
#X is locked for tracks.
x = self.duringTrackDragAndDrop.staticExportItem["guiPosStart"].x()
y = event.scenePos().y()
self.duringTrackDragAndDrop.setPos(x, y)
elif self.duringBlockDragAndDrop:
self.duringBlockDragAndDrop.mouseMoveEventCustom(event)
self.duringBlockDragAndDrop.setPos(event.scenePos())
#self.duringBlockDragAndDrop.mouseMoveEventCustom(event)
super().mouseMoveEvent(event)
@ -493,7 +495,7 @@ class GuiScore(QtWidgets.QGraphicsScene):
elif ccBlock: #TODO: Also different CC in the same GuiTrack
api.moveCCBlockToOtherTrack(dragBlockId, targetTrack.parentDataTrackId, newBlockOrder)
elif event.button() == 1: #a positional mouse left click in a note-track
elif self.parentView.mode() == "notation" and (not (tempBlockDragAndDrop or tempTrackDragAndDrop)) and event.button() == 1: #a positional mouse left click in a note-track, but only it not drag-drop release.
track = self.trackAt(event.scenePos())
if track and not track is self.conductor:
modifiers = QtWidgets.QApplication.keyboardModifiers()

Loading…
Cancel
Save