Browse Source

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

master
Nils 3 years ago
parent
commit
52b9b99007
  1. 18
      engine/api.py
  2. 10
      engine/block.py
  3. 161
      engine/items.py
  4. 5
      engine/main.py
  5. 12
      engine/track.py
  6. 16
      qtgui/items.py
  7. 4
      qtgui/mainwindow.py
  8. 58
      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

5
engine/main.py

@ -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
@ -488,7 +488,8 @@ 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

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:

58
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,19 +79,22 @@ 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"))
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 "))
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 = 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("")
def stretchXCoordinates(self, factor):
"""Reposition the items on the X axis.
@ -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