#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2019, Nils Hilbricht, Germany ( https://www.hilbricht.net ) This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), more specifically its template base application. 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; logging.info("import {}".format(__file__)) 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 """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: oneRectToReturnThemAll = QtCore.QRectF(0,0,0,0) #prevent the annoying "NotImplementError" from Qt for boundingRect. For items that don't need any collision detection. 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) if noteExportObject["dotOnLine"] < 0: #higher than the middle line #self.translate(6, -5) pass else: if noteExportObject["notehead"] <= 1: #whole notes have a bug in the gui because the backend stem info is misleading. Works fine except for ties. pass #TODO #self.translate(6, 0) else: pass #TODO #self.translate(6, 6) 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 GuiPositionMarker(QtWidgets.QGraphicsRectItem): def __init__(self, height, position = 1): """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) 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 paint(self, *args): pass def boundingRect(self, *args): return oneRectToReturnThemAll def __init__(self, upper, lower): super().__init__() #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.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 paint(self, *args): pass def boundingRect(self, *args): return oneRectToReturnThemAll 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) #NOTE: this was from a time when mouse modification was possible. Leave for later use. #def shape(self): # """Qt Function # Return a more accurate shape for this item so that # mouse hovering is more accurate. # # Must be within the bounding rect.""" # return self.path #def boundingRect(self, *args): # """Keep in syn with self.shape()""" # return self.pathRect #def paint(self, *args): #Do NOT implement this. This actually works already. The parent item needs this. 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. #NOTE: this was from a time when mouse modification was possible. Leave for later use. #def shape(self): # """Qt Function # Return a more accurate shape for this item so that # mouse hovering is more accurate. # # Must be within the bounding rect.""" # return self.path #def boundingRect(self, *args): # """Keep in syn with self.shape()""" # return self.pathRect #def paint(self, *args): #Do NOT implement this. This actually works already. The parent item needs this. 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.notehead = GuiRectangleNotehead(parent = self, noteExportObject = noteExportObject) self.velocity = GuiRectangleVelocity(parent = self, noteExportObject = noteExportObject) self.notehead.setZValue(10) #within GuiRectangle def boundingRect(self, *args): return oneRectToReturnThemAll def paint(self, *args): pass class GuiNote(QtWidgets.QGraphicsItem): def __init__(self, noteExportObject, directionRightAndUpwards): super(GuiNote, self).__init__() self.createNote(noteExportObject, directionRightAndUpwards) def paint(self, *args): pass def boundingRect(self, *args): return oneRectToReturnThemAll 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.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(0, constantsAndConfigs.stafflineGap * noteExportObject["dotOnLine"] / 2) 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) for i, (upper, lower) in enumerate(noteExportObject["tuplets"]): tuplet = GuiTupletNumber(upper, lower) if directionRightAndUpwards: tuplet.setPos(3, -6*(i+1)) else: tuplet.setPos(3, 6*(i+1)) tuplet.setParentItem(self.noteHead) if noteExportObject["durationKeyword"]: self.durationKeywordGlyph = GuiNote.durationKeywords[noteExportObject["durationKeyword"]](noteExportObject) if directionRightAndUpwards: self.durationKeywordGlyph.setPos(3, 6) else: self.durationKeywordGlyph.setPos(3, -6) 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 glyph 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(1.5) 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"] if stemOrBeam and stemOrBeam[2] > 0: self.directionRightAndUpwards = False else: self.directionRightAndUpwards = True #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) #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. 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 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"] 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.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) for i, (upper, lower) in enumerate(self.staticItem["tuplets"]): tuplet = GuiTupletNumber(upper, lower) tuplet.setParentItem(self) tuplet.setPos(2, (i+1)*-4-constantsAndConfigs.stafflineGap*2) class GuiKeySignature(GuiItem): 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) glyph.setPos(constantsAndConfigs.magicPixel + i*1.5*constantsAndConfigs.magicPixel, constantsAndConfigs.stafflineGap * accidentalOnLine / 2) #These accidental graphics are shifted to the left in the .svg so they can be placed just as noteheads and match automatically. So we set them +x here. 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 * -1) self.rootGlyph = QtWidgets.QGraphicsSimpleTextItem(constantsAndConfigs.baseNotes[self.staticItem["root"]]) self.rootGlyph.setParentItem(self) self.rootGlyph.setPos(constantsAndConfigs.negativeMagicPixel, 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"), } 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.setPos(-5, -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(-5) 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"), "alto" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefAlto.svg"), "midiDrum" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefPercussion.svg"), "percussion" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefPercussion.svg"), } class GuiTimeSignature(GuiItem): 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): super().__init__(staticItem) if staticItem["oneMeasureInTicks"] == 0: #Basically just a manual barline displayString = "Metrical Off" markerPosition = -2 markerHeight = 4 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 = "Metrical " + r markerHeight = -7 markerPosition = 1 self.text = QtWidgets.QGraphicsSimpleTextItem(displayString) self.text.setParentItem(self) self.text.setPos(0, -7 * constantsAndConfigs.stafflineGap) self.marker = GuiPositionMarker(markerHeight, markerPosition) 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.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) char.setPos(leftBlock.rect().width(), -3*constantsAndConfigs.stafflineGap) 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 is "Chord": return GuiChord(staticItem) elif typ is "Rest": return GuiRest(staticItem) elif typ is "LegatoSlur": return GuiLegatoSlur(staticItem) elif typ is "MultiMeasureRest": return GuiMultiMeasureRest(staticItem) elif typ is "DynamicSignature": return GuiDynamicSignature(staticItem) elif typ is "Clef": return GuiClef(staticItem) elif typ is "KeySignature": return GuiKeySignature(staticItem) #elif typ is "TimeSignature": # return GuiTimeSignature(staticItem) elif typ is "MetricalInstruction": return GuiMetricalInstruction(staticItem) elif typ is "BlockEndMarker": return GuiBlockEndMarker(staticItem) elif typ in ("InstrumentChange", "ChannelChange",): return GuiGenericText(staticItem) else: raise ValueError("Unknown Item Type:", staticItem)