#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), Laborejo2 is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import logging; logger = logging.getLogger(__name__); logger.info("import") from PyQt5 import QtCore, QtGui, QtSvg, QtWidgets from itertools import groupby from engine.items import Chord, KeySignature from .constantsAndConfigs import constantsAndConfigs from template.qtgui.helper import stretchRect import engine.api as api from template.engine import pitch """All svg items (execept pitch-related) graphics are expected to be already on the correct position and will be inserter ON the middle line That means g-clef needs to be 14 pxiels below the y=0 coordinate since these are two stafflines. Or in other words: Open the graphic and scale and reposition it until it fits correctly when inserted with setPos(0,0)""" #Atomic items: class GuiTieCurveGraphicsItem(QtWidgets.QGraphicsPathItem): pen = QtGui.QPen() pen.setCapStyle(QtCore.Qt.RoundCap) pen.setWidth(2) def __init__(self, noteExportObject): """The origin of this item is the scene position. We create a curve by drawing a straight line from zero to end (the local item and the receiving tie item) and then bend it down in the middle, leaving one of the control points empty (0,0).""" super().__init__() self.setPen(GuiTieCurveGraphicsItem.pen) self.setTransform(QtGui.QTransform.fromTranslate(0, -6), True) self.noteExportObject = noteExportObject self.draw() def draw(self): lengthInPixel = self.noteExportObject["tieDistanceInTicks"] / constantsAndConfigs.ticksToPixelRatio path = QtGui.QPainterPath() path.cubicTo(0, 0, lengthInPixel/2, constantsAndConfigs.stafflineGap, lengthInPixel, 0) # ctrlPt1x, ctrlPt1y, ctrlPt2x, ctrlPt2y, endPtx, endPty self.setPath(path) #set path edits the old one in place def stretchXCoordinates(self, factor): """Called directly by the track by iterating through instances""" self.draw() class GuiPositionMarkerOld(QtWidgets.QGraphicsRectItem): def __init__(self, height=3, position = -3): #4, -2 looks like a barline """A simple position marker that connects an annotation above a staff with the staff. Height and position are calculated in stafflines/gaps""" super().__init__(-2, position*constantsAndConfigs.stafflineGap, 1, height * constantsAndConfigs.stafflineGap) #x,y, w, h #self.setBrush(QtCore.Qt.black) #no effect. Should be pen. class GuiPositionMarker(QtWidgets.QGraphicsSimpleTextItem): def __init__(self): """A simple position marker that connects an annotation above a staff with the staff. Height and position are calculated in stafflines/gaps""" super().__init__("⟶") #unicode long arrow U+27F6 self.setRotation(90) self.setScale(1.5) self.setBrush(QtCore.Qt.darkRed) self.setTransform(QtGui.QTransform.fromTranslate(6, -24), True) #shift class GuiTupletNumber(QtWidgets.QGraphicsItem): """The baseline is the scene Position. We position in the up direction""" pen = QtGui.QPen() pen.setColor(QtGui.QColor("darkGrey")) pen.setWidth(1) def boundingRect(self): return self.childrenBoundingRect() def __init__(self, upper, lower): super().__init__() self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden #self.upper = QtWidgets.QGraphicsSimpleTextItem(str(upper)) #self.divider = QtWidgets.QGraphicsLineItem(0,0,10,0) #self.divider.rotate(-70) #self.divider.setPen(GuiTupletNumber.pen) self.lower = QtWidgets.QGraphicsSimpleTextItem(str(lower)) #self.upper.setParentItem(self) #self.divider.setParentItem(self) self.lower.setParentItem(self) #self.upper.setPos(-1,-8) #self.divider.setPos(5,5) #self.lower.setPos(7,-5) self.lower.setPos(-1,-4) self.setScale(0.75) class GuiItem(QtWidgets.QGraphicsItem): """A blank item. Subclass to implement your own""" def __init__(self, staticItem): super().__init__() self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden self.staticItem = staticItem dur = self.staticItem["completeDuration"] exceptions = ["BlockEndMarker"] #Deactivated after stretchX was reimplemented properly as recursive function and not a catch all on all items. I don't think that was used anywhere else. #self.rectangles = [] #Prevents typechecking. only implemented in GuiChord self.pixelPosition = self.staticItem["tickindex"] / constantsAndConfigs.ticksToPixelRatio self.pixelWidth = self.staticItem["completeDuration"] / constantsAndConfigs.ticksToPixelRatio #if dur or self.staticItem["type"] in exceptions: # self.pixelPosition = self.staticItem["tickindex"] / constantsAndConfigs.ticksToPixelRatio # self.pixelWidth = self.staticItem["completeDuration"] / constantsAndConfigs.ticksToPixelRatio #else: # self.pixelPosition = self.staticItem["tickindex"] / constantsAndConfigs.ticksToPixelRatio + constantsAndConfigs.negativeMagicPixel*6 #an offset to the left. this will still get in the way for short notes, but for now it is better than colliding 100% when placed on the note. #TODO: place above the track # self.pixelWidth = 25 #Does not need to be the actual size because this is ONLY important when the item gets inserted at the appending position and the cursor needs to jump "over" the current item, because there is no next one. This is the direct response to the cursor method that sets the appending position as items tickposition+pixelwidth. def updateVisibility(self): """Prevent against typechecking. This method is only implemented in GuiChord""" pass def boundingRect(self): return self.childrenBoundingRect() def stretchXCoordinates(self, factor): """Reposition the items on the X axis. Call goes through all parents/children, starting from ScoreView._stretchXCoordinates. Docstring there.""" pass class GuiRectangleNotehead(QtWidgets.QGraphicsRectItem): """Displays the pitch and actual duration of one note when in rectangle view mode""" def __init__(self, parent, noteExportObject): #Dimensions and Position x = noteExportObject["leftModInTicks"] / constantsAndConfigs.ticksToPixelRatio y = -1 * constantsAndConfigs.stafflineGap / 2 + 1 w = (noteExportObject["rightModInTicks"] + noteExportObject["completeDuration"] - noteExportObject["leftModInTicks"]) / constantsAndConfigs.ticksToPixelRatio h = constantsAndConfigs.stafflineGap #- 2 #2 pixel to make room for the pen-width. super().__init__(x, y, w, h) self.setPos(0, constantsAndConfigs.stafflineGap * noteExportObject["dotOnLine"] / 2) self.setParentItem(parent) #Prepare self.shape() #NOTE: this was from a time when mouse modification was possible. Leave for later use. #self.path = QtGui.QPainterPath() #self.pathRect = QtCore.QRectF(x, y-1, w, h+4) #this is directly related to inits parameter x, y, w, h #self.path.addRect(self.pathRect) #self.setCursor(QtCore.Qt.SizeHorCursor) #Different color when the duration was modified by the user inactive = "green" if noteExportObject["manualOverride"] else "black" self.inactiveColor = QtGui.QColor(inactive) self.setBrush(self.inactiveColor) #Pen for the borders pen = QtGui.QPen() pen.setWidth(0) pen.setColor(QtGui.QColor("darkGrey")) self.setPen(pen) #Since accidentals are part of the notehead we need one here. It doesn't matter if the traditional notehead already has one if noteExportObject["accidental"]: #0 means no difference to keysig self.accidental = GuiNote.createAccidentalGraphicsItem(self, noteExportObject["accidental"]) self.accidental.setPos(0, 0) #not analogue to the notehead acciental position because here we choose the rectangle as parent self.accidental.setParentItem(self) class GuiRectangleVelocity(QtWidgets.QGraphicsRectItem): """Displays the velocity of one note when in rectangle view mode. The Y-position is fixed on the staff and not bound to the rectangle notehead. This makes it possible for the user to compare velocities visually. """ def __init__(self, parent, noteExportObject): #Dimensions and Position x = noteExportObject["leftModInTicks"] / constantsAndConfigs.ticksToPixelRatio #same as GuiRectangleNotehead y = -1 * (noteExportObject["velocity"] / constantsAndConfigs.velocityToPixelRatio) w = (noteExportObject["rightModInTicks"] + noteExportObject["completeDuration"] - noteExportObject["leftModInTicks"]) / constantsAndConfigs.ticksToPixelRatio #same as GuiRectangleNotehead h = noteExportObject["velocity"] / constantsAndConfigs.velocityToPixelRatio #velocityToPixelRation has its own compress factor since values can go from 0 to 127 super().__init__(x, y, w, h) self.setPos(0, 2*constantsAndConfigs.stafflineGap) #The position is fixed to the staff, not to the GuiRectangleNotehead. self.setParentItem(parent) #Prepare self.shape() #NOTE: this was from a time when mouse modification was possible. Leave for later use. #self.path = QtGui.QPainterPath() #self.pathRect = QtCore.QRectF(x, y-1, w, h+4) #this is directly related to inits parameter x, y, w, h #self.path.addRect(self.pathRect) #self.setCursor(QtCore.Qt.SizeHorCursor) #Colors and Apperance inactive = "green" if noteExportObject["velocityManualOverride"] else "lightGray" self.inactiveVelocityColor = QtGui.QColor(inactive) self.activeVelocityColor = QtGui.QColor("darkCyan") self.setBrush(self.inactiveVelocityColor) self.setOpacity(0.3) #Pen for the borders #The pen interferes with the precise appearance we want for live notes pen = QtGui.QPen() pen.setCapStyle(QtCore.Qt.RoundCap) pen.setJoinStyle(QtCore.Qt.RoundJoin) pen.setWidth(2) pen.setColor(QtGui.QColor("black")) self.setPen(pen) #Attach a label that displays the velocity as midi value 0-127 #label = QtWidgets.QGraphicsSimpleTextItem(str(noteExportObject["velocity"])) #label.setPos(3, -3) #label.setParentItem(parent) #Misc Qt Flags self.setAcceptHoverEvents(False) #Make this explicit so it will be remembered that the gui rectangle velocity is a display only. class GuiRectangle(QtWidgets.QGraphicsItem): """An alternative notehead, used to change velocity and duration finetuning. This is a GraphicsItem with two main components: The notehead which displays the duration and pitch of the note The Velocity which displays only the velocity. Both components share their Y position in the staff so they are one item. The notehead rectangles have full opacity and can't layer. The velocity rectangles layer and only a grahpical highlight shows the active one. """ def __init__(self, noteExportObject): super().__init__() self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden self.notehead = GuiRectangleNotehead(parent = self, noteExportObject = noteExportObject) self.velocity = GuiRectangleVelocity(parent = self, noteExportObject = noteExportObject) self.notehead.setZValue(10) #within GuiRectangle def boundingRect(self): return self.childrenBoundingRect() class GuiNote(QtWidgets.QGraphicsItem): def __init__(self, noteExportObject, directionRightAndUpwards): super(GuiNote, self).__init__() self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden self.createNote(noteExportObject, directionRightAndUpwards) def boundingRect(self): return self.childrenBoundingRect() def createNote(self, noteExportObject, directionRightAndUpwards): #note head self.noteExportObject = noteExportObject #self.noteHead = GuiNote.noteheads[noteExportObject["notehead"]]() self.noteHead = self.createNoteheadGraphicsItem(noteExportObject["notehead"]) self.noteHead.setPos(0, constantsAndConfigs.stafflineGap * noteExportObject["dotOnLine"] / 2 - self.noteHead.boundingRect().height()/2) self.noteHead.setParentItem(self) #Ledger lines need to be done in the chord to not be redundant and work for multiple notes. if noteExportObject["accidental"]: #0 means no difference to keysig self.accidental = self.createAccidentalGraphicsItem(noteExportObject["accidental"]) self.accidental.setPos(-1*self.accidental.boundingRect().width()-2, constantsAndConfigs.stafflineGap * noteExportObject["dotOnLine"] / 2 - self.accidental.boundingRect().height()/2) #-height is simply the svg offset self.accidental.setParentItem(self) for dot in range(noteExportObject["dots"]): d = QtSvg.QGraphicsSvgItem(":svg/dot.svg") d.setPos((dot+1)*4+6, constantsAndConfigs.stafflineGap * noteExportObject["dotOnLine"] / 2) d.setParentItem(self) #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, -1.5*constantsAndConfigs.stafflineGap) else: tuplet.setPos(3, 1.5*constantsAndConfigs.stafflineGap) tuplet.setParentItem(self.noteHead) if noteExportObject["durationKeyword"]: self.durationKeywordGlyph = GuiNote.durationKeywords[noteExportObject["durationKeyword"]](noteExportObject) #width = self.noteHead.boundingRect().width() #setPos is relative to noteHead. if directionRightAndUpwards: self.durationKeywordGlyph.setPos(0, -1*constantsAndConfigs.stafflineGap) #x should be the width of the notehead. else: self.durationKeywordGlyph.setPos(0, constantsAndConfigs.stafflineGap+2) self.durationKeywordGlyph.setParentItem(self.noteHead) def createNoteheadGraphicsItem(self, number): item = QtSvg.QGraphicsSvgItem() item.setSharedRenderer(GuiNote.noteheads[number]) return item def createAccidentalGraphicsItem(self, number): item = QtSvg.QGraphicsSvgItem() item.setSharedRenderer(GuiNote.accidentals[number]) return item durationKeywords = { #0 is default and has no markers. api.D_STACCATO : lambda noteExportObject: QtSvg.QGraphicsSvgItem(":svg/scriptsStaccato.svg"), #api.D_TENUTO : lambda noteExportObject: QtWidgets.QGraphicsSimpleTextItem("-"), #TODO: tenuto svg glyph with same position as the others. api.D_TENUTO : lambda noteExportObject: QtSvg.QGraphicsSvgItem(":svg/scriptsTenuto.svg"), api.D_TIE : lambda noteExportObject: GuiTieCurveGraphicsItem(noteExportObject), } noteheads = { 4 : QtSvg.QSvgRenderer(":svg/noteheadsBlack.svg"), #and 8, 16, 32, ... 2 : QtSvg.QSvgRenderer(":svg/noteheadsHalf.svg"), 1 : QtSvg.QSvgRenderer(":svg/noteheadsWhole.svg"), 0 : QtSvg.QSvgRenderer(":svg/noteheadsBrevis.svg"), -1 : QtSvg.QSvgRenderer(":svg/noteheadsLonga.svg"), -2 : QtSvg.QSvgRenderer(":svg/noteheadsMaxima.svg"), } #Accidental .svg are shifted to the left a bit already so we can place them on the same x-coordinates as the notes. accidentals = { 1 : QtSvg.QSvgRenderer(":svg/accidentalsNatural.svg"), 10 : QtSvg.QSvgRenderer(":svg/accidentalsSharp.svg"), 20 : QtSvg.QSvgRenderer(":svg/accidentalsDoublesharp.svg"), -10 : QtSvg.QSvgRenderer(":svg/accidentalsFlat.svg"), -20 : QtSvg.QSvgRenderer(":svg/accidentalsFlatFlat.svg"), } def stretchXCoordinates(self, factor): """Reposition the items on the X axis. Call goes through all parents/children, starting from ScoreView._stretchXCoordinates. Docstring there.""" try: self.durationKeywordGlyph.stretchXCoordinates(factor) except AttributeError: #does not have any attachements or the attachment does not have the stretch function (e.g. staccato) pass #Top level items, all based on GuiItem. class GuiChord(GuiItem): """staticItem is a chord Dict which has another list of dicts: staticItem["notelist"]. Each note with its notehead, accidental etc is in there. This means that a chord can consist of multiple durations as well as pitches, of course. For spacing calculations the chord itself gives us a duration as well.""" def __init__(self, staticItem,): super().__init__(staticItem) self.staticItem = staticItem self.rectangles = [] self.notes = [] self.stem = None #stored for rectangle/head switching self.flag = None #stored for rectangle/head switching self.beamGroupGlyph = None #stored for rectangle/head switching #The actual beams are in the track self.createRectanglesFromData() self.createNotesFromData() self.createGenericFromData() self.updateVisibility() ledgerPen = QtGui.QPen(QtCore.Qt.SolidLine) ledgerPen.setCapStyle(QtCore.Qt.RoundCap) ledgerPen.setWidth(1) stemPen = QtGui.QPen(QtCore.Qt.SolidLine) stemPen.setCapStyle(QtCore.Qt.RoundCap) stemPen.setWidth(2) flags = { #QGraphicsItems can only be used once so we have to save a class constructor here instead of an instance. 1024 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag1024.svg"), #TODO 512 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag512.svg"), #TODO 256 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag256.svg"), 128 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag128.svg"), 64 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag64.svg"), 32 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag32.svg"), 16 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag16.svg"), 8 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag8.svg"), -1024 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag1024i.svg"), #TODO -512 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag512i.svg"), #TODO -256 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag256i.svg"), -128 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag128i.svg"), -64 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag64i.svg"), -32 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag32i.svg"), -16 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag16i.svg"), -8 : lambda: QtSvg.QGraphicsSvgItem(":svg/flag8i.svg"), } def createGenericFromData(self): """Graphics for both rectangles and noteheads""" #Ledger Lines below, above = self.staticItem["ledgerLines"] for i in range(below): line = QtWidgets.QGraphicsLineItem(QtCore.QLineF(0, 0, 12, 0)) line.setPen(GuiChord.ledgerPen) line.setParentItem(self) line.setPos(constantsAndConfigs.negativeMagicPixel, i * constantsAndConfigs.stafflineGap + constantsAndConfigs.stafflineGap*3) for i in range(above): line = QtWidgets.QGraphicsLineItem(QtCore.QLineF(0, 0, 12, 0)) line.setPen(GuiChord.ledgerPen) line.setParentItem(self) line.setPos(constantsAndConfigs.negativeMagicPixel, -1 * i * constantsAndConfigs.stafflineGap - constantsAndConfigs.stafflineGap*3) #Display relative channel changes if not self.staticItem["midiChannelOffset"] == 0: channelGlyph = QtWidgets.QGraphicsSimpleTextItem("ch{0:+}".format(self.staticItem["midiChannelOffset"])) channelGlyph.setScale(0.75) channelGlyph.setParentItem(self) channelGlyph.setPos(0, -5*constantsAndConfigs.stafflineGap) def createRectanglesFromData(self): """Graphics only for rectangles""" for noteExportObject in self.staticItem["notelist"]: if not noteExportObject["tie"] == "notFirst": #Tied notes, except the first, do not produce rectangles because the first note gets an extra large rectangle. rectangle = GuiRectangle(noteExportObject) self.rectangles.append(rectangle) rectangle.setParentItem(self) def createNotesFromData(self): """Graphics only for noteheads""" if self.staticItem["beam"]: stemOrBeam = self.staticItem["beam"] else: stemOrBeam = self.staticItem["stem"] #stem[2] is left/-1 or right/1 stem shifting. #stemOrBeam = (starting point, length, direction) #0 is middle line, but the stem for 0 begins at -1, which is the room above. #2 is treble-g which stem begins at 1. #negative numbers are above the middle line with the same -1 stem offset, compared to the note. #These numbers are calculated by the backend to span the whole chord. We just need to draw the stem here. #For beam groups the length is different for each member because they get shorter/longer with each ascending/descending note if stemOrBeam and stemOrBeam[2] > 0: self.directionRightAndUpwards = False #low notes stemYOffset = 0 else: self.directionRightAndUpwards = True #high notes stemYOffset = -2 #Stem - Both for groups and standalone. if stemOrBeam: #may be an empty tuple for whole notes and brevis line = QtWidgets.QGraphicsLineItem(QtCore.QLineF(0, constantsAndConfigs.stafflineGap * stemOrBeam[1]/2 , 0, 0)) #x1, y1, x2, y2 line.setPen(GuiChord.stemPen) self.stem = line #store as persistent item. Otherwise qt will delete it. line.setParentItem(self) line.setPos(constantsAndConfigs.magicPixel + stemOrBeam[2]*3, constantsAndConfigs.stafflineGap * stemOrBeam[0] / 2 + stemYOffset) #stem[2] is left/-1 or right/1 shifting. #4 is the middle #Flags if not self.staticItem["beam"] and self.staticItem["flag"]: assert self.stem flag = GuiChord.flags[self.staticItem["flag"]]() #the graphic. flag.setParentItem(self) self.flag = flag #store as persistent item. Otherwise qt will delete it. if self.directionRightAndUpwards: #high notes? #self.stem.line().p1().y() are internal local coordinates to the line. It's basically the length flag.setPos(self.stem.pos().x(), self.stem.line().p1().y() + self.stem.pos().y() - flag.boundingRect().height()) else: flag.setPos(self.stem.pos().x(), self.stem.line().p1().y() + self.stem.pos().y()) #we already know where the stem-line is. #Check if this item is the start or end of a beam group and mark it with a textitem (lilypond syntax [ ]) if self.staticItem["beamGroup"]: if self.staticItem["beamGroup"] == "open": beamGroupGlyph = QtWidgets.QGraphicsSimpleTextItem("[") beamGroupGlyph.setParentItem(self) beamGroupGlyph.setPos(0, -5*constantsAndConfigs.stafflineGap) elif self.staticItem["beamGroup"] == "close": beamGroupGlyph = QtWidgets.QGraphicsSimpleTextItem("]") beamGroupGlyph.setParentItem(self) beamGroupGlyph.setPos(constantsAndConfigs.magicPixel, -5*constantsAndConfigs.stafflineGap) self.beamGroupGlyph = beamGroupGlyph #Actual Noteheads lastDotOnLine = float("inf") for noteExportObject in self.staticItem["notelist"]: #notes in a chord come in the order highest to lowest #determine if we have two neighbouring noteheads. If yes we shift one of the heads to the right assert lastDotOnLine >= noteExportObject["dotOnLine"], (lastDotOnLine, noteExportObject["dotOnLine"]) #If this fails means the engine function did not call Chord.notelist.sort() after modification if lastDotOnLine - noteExportObject["dotOnLine"] == 1: moveThisNoteheadToTheRight = True lastDotOnLine = noteExportObject["dotOnLine"] lastDotOnLine += 1 #we shift the current notehead. The next pair would be no problem, even if neighbours. else: moveThisNoteheadToTheRight = False lastDotOnLine = noteExportObject["dotOnLine"] note = GuiNote(noteExportObject, self.directionRightAndUpwards) self.notes.append(note) #we need them later for switching between rectangles and noteheads note.setParentItem(self) if moveThisNoteheadToTheRight: note.setX(note.pos().x() + 6) #6 pixels. def stretchXCoordinates(self, factor): """Reposition the items on the X axis. Call goes through all parents/children, starting from ScoreView._stretchXCoordinates. Docstring there.""" for rectNote in self.rectangles: stretchRect(rectNote.notehead, factor) stretchRect(rectNote.velocity, factor) for note in self.notes: #this is for ties only at the moment, the rest does not stretch note.stretchXCoordinates(factor) def updateVisibility(self): """decide during creation what part is visible at the moment. aka. are we in notehead or rectangle mode. Beams visibility is done in the track function createBeams""" if constantsAndConfigs.noteHeadMode: for r in self.rectangles: r.hide() for n in self.notes: n.show() if self.flag: self.flag.show() if self.stem: self.stem.show() if self.beamGroupGlyph: #This is about the start and end marker [ ] .The beam itself is in the track self.beamGroupGlyph.show() else: #rectangle mode. for r in self.rectangles: r.show() for n in self.notes: n.hide() if self.flag: self.flag.hide() if self.stem: self.stem.hide() if self.beamGroupGlyph: #This is about the start and end marker [ ] .The beam itself is in the track self.beamGroupGlyph.hide() class GuiLiveNote(QtWidgets.QGraphicsRectItem): """A pure rectangle note, meant for live recording.""" #TODO: Velocity indicator? instances = [] def __init__(self, parent, liveNoteData): self.__class__.instances.append(self) self.liveNoteData = liveNoteData #Dimensions and Position x = 0 #A live note is always literally where it sounds y = -1 * constantsAndConfigs.stafflineGap / 2 w = liveNoteData["duration"] / constantsAndConfigs.ticksToPixelRatio h = constantsAndConfigs.stafflineGap super().__init__(x, y, w, h) self.setY(constantsAndConfigs.stafflineGap * liveNoteData["dotOnLine"] / 2) self.setParentItem(parent) self.setBrush(QtGui.QColor("red")) #Pen for the borders pen = QtGui.QPen() pen.setWidth(0) self.setPen(pen) if liveNoteData["accidental"]: #0 means no difference to keysig self.accidental = GuiNote.createAccidentalGraphicsItem(self, liveNoteData["accidental"]) self.accidental.setPos(0, 0) #not analogue to the notehead acciental position because here we choose the rectangle as parent self.accidental.setParentItem(self) def update(self, duration): r = self.rect() r.setRight(duration / constantsAndConfigs.ticksToPixelRatio) self.setRect(r) class GuiRest(GuiItem): def __init__(self, staticItem): super(GuiRest, self).__init__(staticItem) self.createGraphicItemsFromData() rests = { #QGraphicsItems can only be used once so we have to save a class constructor here instead of an instance. -1 : lambda: QtSvg.QGraphicsSvgItem(":svg/restLonga.svg"), -2 : lambda: QtSvg.QGraphicsSvgItem(":svg/restMaxima.svg"), 0 : lambda: QtSvg.QGraphicsSvgItem(":svg/restBrevis.svg"), 1 : lambda: QtSvg.QGraphicsSvgItem(":svg/rest1.svg"), 2 : lambda: QtSvg.QGraphicsSvgItem(":svg/rest2.svg"), 4 : lambda: QtSvg.QGraphicsSvgItem(":svg/rest4.svg"), 8 : lambda: QtSvg.QGraphicsSvgItem(":svg/rest8.svg"), 16 : lambda: QtSvg.QGraphicsSvgItem(":svg/rest16.svg"), 32 : lambda: QtSvg.QGraphicsSvgItem(":svg/rest32.svg"), 64 : lambda: QtSvg.QGraphicsSvgItem(":svg/rest64.svg"), 128 : lambda: QtSvg.QGraphicsSvgItem(":svg/rest128.svg"), 256 : lambda: QtSvg.QGraphicsSvgItem(":svg/rest256.svg"), #TODO 512 : lambda: QtSvg.QGraphicsSvgItem(":svg/rest512.svg"), #TODO 1024 : lambda: QtSvg.QGraphicsSvgItem(":svg/rest1024.svg"), #TODO } def createGraphicItemsFromData(self): self.glyph = GuiRest.rests[self.staticItem["rest"]]() self.glyph.setY(-1*constantsAndConfigs.stafflineGap) self.glyph.setParentItem(self) for dot in range(self.staticItem["dots"]): d = QtSvg.QGraphicsSvgItem(":svg/dot.svg") d.setPos((dot+1)*4+6, -2) d.setParentItem(self) if self.staticItem["tuplet"]: upper, lower = self.staticItem["tuplet"] tuplet = GuiTupletNumber(upper, lower) tuplet.setParentItem(self) tuplet.setPos(2, -4-constantsAndConfigs.stafflineGap*2) class GuiKeySignature(GuiItem): pitchModYOffset = { #The svg graphics are not aligned "musically". We need to adjust their positions. 0 : -1.25, 10 : -1.25, 20 : -0.5, -10 : -1.75, -20 : -1.75, } def __init__(self, staticItem): super(GuiKeySignature, self).__init__(staticItem) self.createGraphicItemsFromData() def createGraphicItemsFromData(self): """staticItems accidentalsOnLines is ordered. We receive the correct accidental order from left to right and only need to place the gaps""" for i, (accidentalOnLine, pitchMod) in enumerate(self.staticItem["accidentalsOnLines"]): glyph = GuiKeySignature.accidentals[pitchMod]() glyph.setParentItem(self) x = constantsAndConfigs.magicPixel + i*1.5*constantsAndConfigs.magicPixel y = (accidentalOnLine/2) * constantsAndConfigs.stafflineGap + GuiKeySignature.pitchModYOffset[pitchMod]*constantsAndConfigs.stafflineGap glyph.setPos(x, y) if not self.staticItem["accidentalsOnLines"]: #No accidentals in the keysig (C-Major, D-Dorian etc.) gets a big natural sign. self.bigNatural = GuiKeySignature.accidentals[0]() #"big natural"... hö hö hö hö self.bigNatural.setParentItem(self) self.bigNatural.setPos(constantsAndConfigs.magicPixel, constantsAndConfigs.stafflineGap * GuiKeySignature.pitchModYOffset[0]) self.rootGlyph = QtWidgets.QGraphicsSimpleTextItem(pitch.baseNotesToAccidentalNames[self.staticItem["root"]]) self.rootGlyph.setParentItem(self) self.rootGlyph.setPos(1, 2*constantsAndConfigs.stafflineGap) accidentals = { #QGraphicsItems can only be used once so we have to save a class constructor here instead of an instance. 0 : lambda: QtSvg.QGraphicsSvgItem(":svg/accidentalsNatural.svg"), #normaly we don't receive those, but it is possible to ask for explicit naturals in the backend and in lilypond. 10 : lambda: QtSvg.QGraphicsSvgItem(":svg/accidentalsSharp.svg"), 20 : lambda: QtSvg.QGraphicsSvgItem(":svg/accidentalsDoublesharp.svg"), -10 : lambda: QtSvg.QGraphicsSvgItem(":svg/accidentalsFlat.svg"), -20 : lambda: QtSvg.QGraphicsSvgItem(":svg/accidentalsFlatFlat.svg"), #TODO: Triple Sharps and Flats are not supported. But better wrong glyph than a crash: -30 : lambda: QtSvg.QGraphicsSvgItem(":svg/accidentalsNatural.svg"), 30 : lambda: QtSvg.QGraphicsSvgItem(":svg/accidentalsNatural.svg"), } class GuiClef(GuiItem): def __init__(self, staticItem): super(GuiClef, self).__init__(staticItem) self.createGraphicItemsFromData() def createGraphicItemsFromData(self): self.glyph = GuiClef.clefs[self.staticItem["clef"]]() self.glyph.setParentItem(self) self.glyph.setScale(0.5) self.glyph.setPos(-1*self.glyph.boundingRect().width()/2, -1 * self.glyph.boundingRect().height()/2 - 3.5*constantsAndConfigs.stafflineGap) #self.text = QtWidgets.QGraphicsSimpleTextItem(self.staticItem["clef"].title()) #self.text.setParentItem(self) #self.text.setPos(0, -5 * constantsAndConfigs.stafflineGap) self.marker = GuiPositionMarker() self.marker.setParentItem(self) self.marker.setPos(0, 0) clefs = { #QGraphicsItems can only be used once so we have to save a class constructor here instead of an instance. "treble" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefTreble.svg"), "treble_8" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefTreble_8.svg"), "treble^8" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefTreble^8.svg"), "bass" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefBass.svg"), "bass_8" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefBass_8.svg"), "bass^8" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefBass^8.svg"), "alto" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefAlto.svg"), "midiDrum" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefPercussion.svg"), "percussion" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefPercussion.svg"), } #If you want to position the clef on the staff lines use these. #We need to have different one for each graphics because the graphics just start at y=0 without any musical "knowledge" cleffYOnStaff = { "treble" : -3.5*constantsAndConfigs.stafflineGap, "treble_8" : -3.5*constantsAndConfigs.stafflineGap, "treble^8" : -5*constantsAndConfigs.stafflineGap, "bass" : -2*constantsAndConfigs.stafflineGap, "bass_8" : -2*constantsAndConfigs.stafflineGap, "bass^8" : -3*constantsAndConfigs.stafflineGap, "alto" : -1.75*constantsAndConfigs.stafflineGap, "midiDrum" : -0.85*constantsAndConfigs.stafflineGap, "percussion" : -0.85*constantsAndConfigs.stafflineGap, } class GuiTimeSignature(GuiItem): """leave this in the code because of the cool svg subrender feature""" def __init__(self, staticItem): raise Exception("Don't use this item. It will be deleted in future versions. Use a Metrical Instruction") super().__init__(staticItem) self.setPos(0, constantsAndConfigs.stafflineGap) self.createGraphicItemsFromData() def createGraphicItemsFromData(self): #higher number, how many notes for i, charInt in enumerate(str(self.staticItem["nominator"])): n = GuiTimeSignature.digits[int(charInt)] charInt = QtSvg.QGraphicsSvgItem() charInt.setSharedRenderer(GuiTimeSignature.renderer) charInt.setElementId(n) charInt.setPos(i*2*constantsAndConfigs.magicPixel, -1*constantsAndConfigs.stafflineGap) charInt.setParentItem(self) #lower number, which note type for i, charInt in enumerate(str(self.staticItem["denominator"])): #2^n n = GuiTimeSignature.digits[int(charInt)] charInt = QtSvg.QGraphicsSvgItem() charInt.setSharedRenderer(GuiTimeSignature.renderer) charInt.setElementId(n) charInt.setPos(i*2*constantsAndConfigs.magicPixel, constantsAndConfigs.stafflineGap) charInt.setParentItem(self) renderer = QtSvg.QSvgRenderer(":svg/numbers.svg") digits = "zero one two three four five six seven eight nine".split() #a list class GuiMetricalInstruction(GuiItem): def __init__(self, staticItem, drawMarker=True): super().__init__(staticItem) if staticItem["oneMeasureInTicks"] == 0: #Basically just a manual barline displayString = "X" else: #Format the Tree of Instructions realTree = eval(staticItem["treeOfInstructions"]) if not type(realTree[0]) is tuple: #first order measure. like 3/4 or 2/4 or anything with just one stressed position realTree = (realTree, ) #created nested iterator r = self.treeToPrettyString(realTree) r = r.__repr__()[1:-1].replace("'", "") onlyTopLevelElements = not any(isinstance(el, list) for el in r) if onlyTopLevelElements: r = r.replace(",", "") displayString = "M " + r #A metrical instruction is by definition at the beginning of a measure #We therefore need to place the text above the barnumber self.text = QtWidgets.QGraphicsSimpleTextItem(displayString) self.text.setParentItem(self) self.text.setPos(-6, -7 * constantsAndConfigs.stafflineGap) #by definition the metrical sig is in the same position as the measure number. Shift high up if drawMarker: self.marker = GuiPositionMarker() self.marker.setParentItem(self) self.marker.setPos(0,0) def treeToPrettyString(self, tree): """Convert a metrical instruction into a pretty string with note symbols For example ((53760, 53760), (53760, 53760)) which is ((D4, D4), (D4, D4)) into 2𝅘𝅥 2𝅘𝅥 """ result = [] for element in tree: if type(element) is tuple: l = self.treeToPrettyString(element) l = ["{}{}".format(sum(1 for e in grouper), note) for note, grouper in groupby(l)] if len(l) == 1: l = l[0] #makes formatting clearerm result.append(l) else: assert type(element) is int noteString = constantsAndConfigs.realNoteDisplay[element] result.append(noteString) return result class GuiBlockEndMarker(GuiItem): def __init__(self, staticItem): super(GuiBlockEndMarker, self).__init__(staticItem) self.glyph = QtSvg.QGraphicsSvgItem(":svg/blockEnd.svg") self.glyph.setPos(-2, -1*self.glyph.boundingRect().height()/2) self.createGraphicItemsFromData() def createGraphicItemsFromData(self): self.glyph.setParentItem(self) class GuiDynamicSignature(GuiItem): def __init__(self, staticItem): super(GuiDynamicSignature, self).__init__(staticItem) self.glyph = QtWidgets.QGraphicsSimpleTextItem(staticItem["keyword"]) self.createGraphicItemsFromData() def createGraphicItemsFromData(self): self.glyph.setPos(0, 5*constantsAndConfigs.stafflineGap) self.glyph.setParentItem(self) class GuiMultiMeasureRest(GuiItem): def __init__(self, staticItem): super(GuiMultiMeasureRest, self).__init__(staticItem) self.createGraphicItemsFromData() def createGraphicItemsFromData(self): w = self.staticItem["completeDuration"] / constantsAndConfigs.ticksToPixelRatio - constantsAndConfigs.magicPixel*2 if int(self.staticItem["numberOfMeasures"]) == self.staticItem["numberOfMeasures"]: #just an int? numberOfMeasuresString = str(int(self.staticItem["numberOfMeasures"])) else: numberOfMeasuresString = str(self.staticItem["numberOfMeasures"]) #include the decimal dot rect = QtWidgets.QGraphicsRectItem(constantsAndConfigs.magicPixel,0, w, constantsAndConfigs.stafflineGap) #x,y, w, h rect.setBrush(QtCore.Qt.black) rect.setParentItem(self) self.rect = rect #for stretching leftBlock = QtWidgets.QGraphicsRectItem(2, -1* constantsAndConfigs.stafflineGap, constantsAndConfigs.magicPixel, 3*constantsAndConfigs.stafflineGap) leftBlock.setBrush(QtCore.Qt.black) leftBlock.setParentItem(self) rightBlock = QtWidgets.QGraphicsRectItem(0, -1* constantsAndConfigs.stafflineGap, constantsAndConfigs.magicPixel, 3*constantsAndConfigs.stafflineGap) #x, y, w, h rightBlock.setBrush(QtCore.Qt.black) #rightBlock.setPos(w,0) rightBlock.setPos(rect.rect().right(),0) rightBlock.setParentItem(self) self.rightBlock = rightBlock #for stretching for i, char in enumerate(numberOfMeasuresString): if char == ".": continue #TODO: Maybe warn about that? MMR below 1 are not valid but they are needed for editing purposes. Sometimes you need to go through such a value. else: n = GuiTimeSignature.digits[int(char)] char = QtSvg.QGraphicsSvgItem() char.setSharedRenderer(GuiTimeSignature.renderer) char.setElementId(n) char.setPos(i*4*constantsAndConfigs.magicPixel + 4* constantsAndConfigs.magicPixel, -3*constantsAndConfigs.stafflineGap) #this was older but commented out without given a reason. Now it is back. #char.setPos(leftBlock.rect().width(), -3*constantsAndConfigs.stafflineGap) #This was newer, but was only good for single digits char.setScale(1.5) char.setParentItem(self) self.setPos(0, -3 * constantsAndConfigs.stafflineGap) def stretchXCoordinates(self, factor): """Reposition the items on the X axis. Call goes through all parents/children, starting from ScoreView._stretchXCoordinates. Docstring there.""" stretchRect(self.rect, factor) self.rightBlock.setX(self.rightBlock.pos().x() * factor) #self.char.setX(self.char.pos().x() * factor) class GuiLegatoSlur(GuiItem): def __init__(self, staticItem): super().__init__(staticItem) if staticItem["slur"] == "open": self.glyph = QtWidgets.QGraphicsSimpleTextItem("(") self.glyph.setPos(2,-5*constantsAndConfigs.stafflineGap) else: assert staticItem["slur"] == "close" self.glyph = QtWidgets.QGraphicsSimpleTextItem(")") self.glyph.setPos(-4,-5*constantsAndConfigs.stafflineGap) self.createGraphicItemsFromData() def createGraphicItemsFromData(self): #self.glyph.setPos(0, -5*constantsAndConfigs.stafflineGap) self.glyph.setParentItem(self) class GuiGenericText(GuiItem): def __init__(self, staticItem): super().__init__(staticItem) self.glyph = QtWidgets.QGraphicsSimpleTextItem(staticItem["UIstring"]) self.glyph.setPos(0, -5*constantsAndConfigs.stafflineGap) self.glyph.setParentItem(self) def staticItem2Item(staticItem): typ = staticItem["type"] if typ == "Chord": return GuiChord(staticItem) elif typ == "Rest": return GuiRest(staticItem) elif typ == "LegatoSlur": return GuiLegatoSlur(staticItem) elif typ == "MultiMeasureRest": return GuiMultiMeasureRest(staticItem) elif typ == "DynamicSignature": return GuiDynamicSignature(staticItem) elif typ == "Clef": return GuiClef(staticItem) elif typ == "KeySignature": return GuiKeySignature(staticItem) #elif typ == "TimeSignature": # return GuiTimeSignature(staticItem) elif typ == "MetricalInstruction": return GuiMetricalInstruction(staticItem) elif typ == "BlockEndMarker": return GuiBlockEndMarker(staticItem) elif typ in ("InstrumentChange", "ChannelChange", "LilypondText"): return GuiGenericText(staticItem) else: raise ValueError("Unknown Item Type:", staticItem)