You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

918 lines
44 KiB

#! /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 <http://www.gnu.org/licenses/>.
"""
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() #1000 !? Weird svg.
#setPos is relative to noteHead.
if directionRightAndUpwards:
#self.durationKeywordGlyph.setScale(-1) #doesn't work because center of rotation is somehwere at pixel 1000!!
self.durationKeywordGlyph.setPos(0, -1*constantsAndConfigs.stafflineGap) #x should be the width of the notehead.
else:
self.durationKeywordGlyph.setPos(0, constantsAndConfigs.stafflineGap-1)
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 = 2
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):
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(pitch.baseNotesToAccidentalNames[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"),
#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"),
"alto" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefAlto.svg"),
"midiDrum" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefPercussion.svg"),
"percussion" : lambda: QtSvg.QGraphicsSvgItem(":svg/clefPercussion.svg"),
}
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):
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
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)