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.
883 lines
41 KiB
883 lines
41 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2020, 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 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 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.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(pitch.baseNotesToBaseNames[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 = "X"
|
|
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
|
|
markerPosition = -2
|
|
markerHeight = 4
|
|
|
|
|
|
#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(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", "LilypondText"):
|
|
return GuiGenericText(staticItem)
|
|
else:
|
|
raise ValueError("Unknown Item Type:", staticItem)
|
|
|