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.
611 lines
30 KiB
611 lines
30 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")
|
|
|
|
#Standard Library
|
|
from math import log
|
|
|
|
#Third party
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
translate = QtCore.QCoreApplication.translate
|
|
|
|
#Template
|
|
from template.qtgui.helper import stringToColor
|
|
from template.qtgui.helper import stretchLine, stretchRect, callContextMenu, removeInstancesFromScene
|
|
|
|
#Our own files
|
|
import engine.api as api
|
|
|
|
from . import graphs
|
|
from .items import staticItem2Item, GuiTieCurveGraphicsItem, GuiClef, GuiKeySignature, GuiMetricalInstruction
|
|
from .constantsAndConfigs import constantsAndConfigs
|
|
from .submenus import BlockPropertiesEdit
|
|
|
|
|
|
cosmeticPen = QtGui.QPen()
|
|
cosmeticPen.setCosmetic(True)
|
|
|
|
sceneXOffsetForInitialSigs = -75
|
|
|
|
class GuiBlockHandle(QtWidgets.QGraphicsRectItem):
|
|
"""A simplified version of a Block. Since we don't use blocks in the GUI, only in the backend
|
|
we still need them sometimes as macro strutures, where we don't care about the content.
|
|
This is the transparent Block handle that appears when the user uses the mouse to drag and drop
|
|
a block.
|
|
|
|
It is visible all the time though and can be clicked on. In opposite to the background color,
|
|
which is just a color and stays in place.
|
|
"""
|
|
def __init__(self, parent, staticExportItem, x, y, w, h):
|
|
super().__init__(x, y, w, h) #x and y are coordinates relative to its parent block. Y will always be a fixed value. At the moment of writing -14.
|
|
#self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden
|
|
#self.setFlag(QtWidgets.QGraphicsItem.ItemContainsChildrenInShape, True)
|
|
self.parent = parent #GuiTrack instance
|
|
self.parentGuiTrack = parent #redundant, but specifically for block movement. see ScoreScene
|
|
self.trans = QtGui.QColor("transparent")
|
|
self.setPen(self.trans) #activate to show outline.
|
|
self.setBrush(self.trans)
|
|
#self.setOpacity(0.4) #slightly fuller than background
|
|
#self.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True)
|
|
#self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
|
|
self.setParentItem(parent)
|
|
#self.setZValue(10) #This is the z value within GuiTrack
|
|
self.staticExportItem = staticExportItem
|
|
self._color = None # @property
|
|
self.posBeforeMove = None
|
|
self.cursorPosOnMoveStart = None
|
|
self.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
|
|
|
|
|
|
#Display Block ID
|
|
"""
|
|
self.idText = QtWidgets.QGraphicsSimpleTextItem(str(self.staticExportItem["id"]))
|
|
self.idText.setParentItem(self)
|
|
self.idText.setPos(0, constantsAndConfigs.stafflineGap)
|
|
#self.idText.setScale(4)
|
|
self.idText.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
|
|
"""
|
|
|
|
|
|
self.startLabel = QtWidgets.QGraphicsSimpleTextItem(self.staticExportItem["name"])
|
|
self.endLabel = QtWidgets.QGraphicsSimpleTextItem(self.staticExportItem["name"] + translate("musicstructures", " end "))
|
|
|
|
self.startLabel.setParentItem(self)
|
|
self.startLabel.setPos(0, constantsAndConfigs.stafflineGap)
|
|
self.startLabel.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
|
|
#self.startLabel.setZValue(self.zValue()+1) this doesn't matter because our value is relative to self, the rectangle. And selfs value is relative to the GuiTracks other children.
|
|
|
|
self.endLabel.setParentItem(self)
|
|
self.endLabel.setPos(self.rect().width() - self.endLabel.boundingRect().width(), constantsAndConfigs.stafflineGap)
|
|
self.endLabel.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
|
|
|
|
|
|
@property
|
|
def color(self):
|
|
assert self._color
|
|
return self._color
|
|
|
|
@color.setter
|
|
def color(self, c:QtGui.QColor):
|
|
"""QColor inserted by the creating function in GuiTrack.
|
|
Only available during mousePressEvent. Used during dragging, then reset to transparent.
|
|
|
|
This is on every change. Every note."""
|
|
self._color = c
|
|
if c.lightness() > 127: #between 0 (for black) and 255 (for white)
|
|
labelColor = QtGui.QColor("black")
|
|
else:
|
|
labelColor = QtGui.QColor("white")
|
|
self.startLabel.setBrush(labelColor)
|
|
self.endLabel.setBrush(labelColor)
|
|
|
|
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, factor)
|
|
self.endLabel.setPos(self.rect().width() - self.endLabel.boundingRect().width(), constantsAndConfigs.stafflineGap)
|
|
|
|
def blockMode(self):
|
|
self.startLabel.show()
|
|
self.endLabel.show()
|
|
|
|
def itemMode(self):
|
|
self.startLabel.hide()
|
|
self.endLabel.hide()
|
|
|
|
|
|
|
|
def mousePressEventCustom(self, event):
|
|
"""Not a qt-override. This is called directly by GuiScore
|
|
if you click on a block"""
|
|
self.posBeforeMove = self.pos()
|
|
self.cursorPosOnMoveStart = QtGui.QCursor.pos()
|
|
self.setBrush(self.color)
|
|
self.endLabel.hide()
|
|
super().mousePressEvent(event)
|
|
|
|
#Mouse Move Event, as in dragging the blocks around, in in scorescene.py
|
|
#because we need to drag blocks into another track.
|
|
|
|
def mouseReleaseEventCustom(self, event):
|
|
"""Not a qt-override. This is called directly by GuiScore
|
|
if you click-release on a block"""
|
|
self.setBrush(self.trans)
|
|
self.setPos(self.posBeforeMove) #In case the block was moved to a position where no track is (below the tracks) we just reset the graphics.
|
|
self.posBeforeMove = None
|
|
self.cursorPosOnMoveStart = None
|
|
self.endLabel.show()
|
|
super().mouseReleaseEvent(event)
|
|
|
|
def contextMenuEventCustom(self, event):
|
|
"""The original context menu was too unreliable. We now call it
|
|
directly in Track.
|
|
|
|
We do not use event parameter. This is for compatibility.
|
|
"""
|
|
if self.startLabel.isVisible(): #block mode
|
|
listOfLabelsAndFunctions = [
|
|
(translate("musicstructures", "edit properties"), lambda: BlockPropertiesEdit(self.scene().parentView.mainWindow, staticExportItem = self.staticExportItem)),
|
|
("separator", None),
|
|
#("split here", lambda: self.splitHere(event)), #Impossible because we can't see notes.
|
|
(translate("musicstructures", "duplicate"), lambda: api.duplicateBlock(self.staticExportItem["id"])),
|
|
(translate("musicstructures", "duplicate to reserved empty"), lambda: api.duplicateToReservedSpaceBlock(self.staticExportItem["id"])),
|
|
(translate("musicstructures", "create content link"), lambda: api.duplicateContentLinkBlock(self.staticExportItem["id"])),
|
|
(translate("musicstructures", "unlink"), lambda: api.unlinkBlock(self.staticExportItem["id"])),
|
|
("separator", None),
|
|
(translate("musicstructures", "move to start"), lambda: api.moveBlockToStartOfTrack(self.staticExportItem["id"])),
|
|
(translate("musicstructures", "move to end"), lambda: api.moveBlockToEndOfTrack(self.staticExportItem["id"])),
|
|
("separator", None),
|
|
(translate("musicstructures", "join with next block"), lambda: api.joinBlockWithNext(self.staticExportItem["id"])),
|
|
(translate("musicstructures", "delete block"), lambda: api.deleteBlock(self.staticExportItem["id"])),
|
|
("separator", None),
|
|
(translate("musicstructures", "append block at the end"), lambda: api.appendBlock(self.parent.staticExportItem["id"])),
|
|
]
|
|
callContextMenu(listOfLabelsAndFunctions)
|
|
|
|
class GuiTrack(QtWidgets.QGraphicsItem):
|
|
"""In opposite to tracks and block(backgrounds and handles) tracks never get recreated.
|
|
Init is called once on creation, not virtually on every update.
|
|
However, the track children, its parts, get deleted and recreated very often.
|
|
|
|
|
|
Order of creation:
|
|
init
|
|
stafflines
|
|
second init
|
|
update edit mode
|
|
-> then api takes over and instructs the following methods through callbacks
|
|
|
|
Typical order of method calls for updates:
|
|
barlines
|
|
beams
|
|
stafflines
|
|
items
|
|
redraw
|
|
background color
|
|
update edit mode (not on every item update, but at least on load)
|
|
"""
|
|
|
|
def __init__(self, parentScore, staticExportItem):
|
|
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.setFlag(QtWidgets.QGraphicsItem.ItemContainsChildrenInShape, True) #Do not use! If we activate this a block will not be able to cross over into another track with drag and drop
|
|
|
|
self.parentScore = parentScore
|
|
self.staticExportItem = staticExportItem #This is not the notes but the track meta data. The notes are called staticRepresentationList
|
|
self.items = [] #this is used for stretching and processing of the current items. scene clear is done diffently. See self.createGraphicItemsFromData
|
|
self.barLines = []
|
|
self.beams = []
|
|
self.staffLines = []
|
|
self.staticItems = [] #not deleted automatically by callbacks.
|
|
self.ItemIgnoresTransformations = [] #not literally colors. QRectItems with a color. created in self.paintBlockBackgroundColors()
|
|
self.transparentBlockHandles = [] #list of GuiBlockHandle in order. Only appear when the mouse is used to drag and drop.
|
|
self.backgroundBlockColors = [] #QGraphicsRectItems. Always visible.
|
|
self.lengthInPixel = 0 # a cached value
|
|
self.createStaffLines() #no stafflines at all are too confusing.
|
|
self.ccPaths = {} # ccNumber0-127:PathItem. Empty for a new track. We only create ccPaths with the first ccBlock. Creation and handling is done in GuiScore, starting with syncCCsToBackend.
|
|
self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, True)
|
|
|
|
self.nameGraphic = self.NameGraphic("", parent = self)
|
|
self.staticItems.append(self.nameGraphic)
|
|
self.nameGraphic.updateName(self.staticExportItem)
|
|
self.blockModeNameGraphic = self.NameGraphic("", parent = self) #additional name graphic in block mode that is always visible
|
|
self.staticItems.append(self.blockModeNameGraphic)
|
|
self.blockModeNameGraphic.updateName(self.staticExportItem)
|
|
|
|
#Add one central "Create new CC Path" button which is for all non-existing CC Paths of this track and reacte to the current constantsAndConfig.ccValue
|
|
#This button is not in the CCPath object because those only get created for existing backend-CCs.
|
|
self.universalCreateFirstCCBlock = QtWidgets.QGraphicsSimpleTextItem(translate("musicstructures", "Create CC Path"))
|
|
self.universalCreateFirstCCBlock.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity) #toggle between edit modes not only hides stuff but the track itself gets 10% opacity. We want to avoid it for this item.
|
|
self.universalCreateFirstCCBlock.mousePressEvent = lambda mouseEvent: api.newGraphTrackCC(trId = self.staticExportItem["id"], cc = constantsAndConfigs.ccViewValue) #trigger callback to self.syncCCsToBackend
|
|
self.universalCreateFirstCCBlock.setParentItem(self)
|
|
self.universalCreateFirstCCBlock.setPos(0,0) #for now. Afterwards it gets updated by updateBlocks .
|
|
self.universalCreateFirstCCBlock.setZValue(10)
|
|
|
|
#self.secondStageInitNowThatWeHaveAScene gets called by the ScoreScene.redraw(), where new tracks get created. After it was inserted into the scene.
|
|
|
|
|
|
class NameGraphic(QtWidgets.QGraphicsSimpleTextItem):
|
|
"""The update callback is in scoreScene.redraw()"""
|
|
|
|
|
|
def __init__(self, text, parent):
|
|
super().__init__(text)
|
|
self.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
|
|
self.parent = parent
|
|
self.setParentItem(parent)
|
|
|
|
def _editName(self):
|
|
result = QtWidgets.QInputDialog.getText(self.scene().parentView, translate("musicstructures", "Track Name"), #dialog title
|
|
translate("musicstructures", "Set Track Name for {}").format(self.parent.staticExportItem["id"]), #label
|
|
QtWidgets.QLineEdit.Normal,
|
|
self.parent.staticExportItem["name"]
|
|
)
|
|
if result[1]:
|
|
api.setTrackName(self.parent.staticExportItem["id"], nameString = result[0], initialInstrumentName = self.parent.staticExportItem["initialInstrumentName"], initialShortInstrumentName = self.parent.staticExportItem["initialShortInstrumentName"]) #keep the old lilypond names
|
|
|
|
def contextMenuEvent(self, event):
|
|
listOfLabelsAndFunctions = [ (translate("musicstructures", "edit name"), self._editName), ]
|
|
callContextMenu(listOfLabelsAndFunctions)
|
|
event.accept()
|
|
|
|
def updateName(self, trackExportObject:dict):
|
|
self.setText(f"Track {trackExportObject['index']+1} - {trackExportObject['name']}")
|
|
|
|
|
|
def secondStageInitNowThatWeHaveAScene(self):
|
|
"""ScoreScene.redraw() calls this after the track was inserted into the scene and therefore
|
|
has a position, opacity, parent item etc. (All of that is not read in normal __init__)"""
|
|
pass
|
|
|
|
|
|
def boundingRect(self):
|
|
"""Without bounding rect not mousePressEvents for children"""
|
|
if self.staticExportItem["double"]: #double track, twice as high
|
|
h = 10 * constantsAndConfigs.stafflineGap
|
|
else:
|
|
h = 4 * constantsAndConfigs.stafflineGap
|
|
|
|
w = self.lengthInPixel
|
|
|
|
return QtCore.QRectF(0,0,w,h) #x,y,w,h - relative to self, so pos is always 0,0
|
|
|
|
def toggleNoteheadsRectangles(self):
|
|
for item in self.items:
|
|
item.updateVisibility()
|
|
for beam in self.beams:
|
|
beam.rectangle.setVisible(constantsAndConfigs.noteHeadMode)
|
|
|
|
def redraw(self, staticRepresentationList):
|
|
"""Called very often. For example after each note insert into the track.
|
|
But not after a mode change"""
|
|
self.createGraphicItemsFromData(staticRepresentationList)
|
|
self.nameGraphic.setPos(30 + self.lengthInPixel, -1*constantsAndConfigs.stafflineGap) #self.lengthInPixel is now up to date
|
|
|
|
def centerBlockModeNameGraphic(self, centerSceneX):
|
|
"""Attached to the horizontal scrollbar.
|
|
Instead of calculating X in every track we do it once in parentScore.reactToHorizontalScroll()"""
|
|
self.blockModeNameGraphic.setPos(centerSceneX, (-5*constantsAndConfigs.stafflineGap)) #y is in track coordinates
|
|
|
|
def paintBlockBackgroundColors(self, staticBlocksRepresentation):
|
|
"""This gets not called by self.createGraphicItemsFromData but only by
|
|
a score callback for blocksChanged"""
|
|
for bg in self.backgroundBlockColors:
|
|
self.parentScore.removeWhenIdle(bg)
|
|
for th in self.transparentBlockHandles:
|
|
self.parentScore.removeWhenIdle(th)
|
|
self.backgroundBlockColors = []
|
|
self.transparentBlockHandles = []
|
|
|
|
for block in staticBlocksRepresentation:
|
|
if self.staticExportItem["double"]: #double track, twice as high
|
|
h = 10 * constantsAndConfigs.stafflineGap
|
|
else:
|
|
h = 4 * constantsAndConfigs.stafflineGap
|
|
bgItem = QtWidgets.QGraphicsRectItem(0, 0, block["completeDuration"] / constantsAndConfigs.ticksToPixelRatio, h) #x, y, w, h
|
|
bgItem.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
|
|
bgItem.setPen(QtGui.QColor("transparent"))
|
|
color = stringToColor(block["name"])
|
|
bgItem.setBrush(color)
|
|
bgItem.setParentItem(self)
|
|
self.backgroundBlockColors.append(bgItem)
|
|
bgItem.setPos(block["tickindex"] / constantsAndConfigs.ticksToPixelRatio, -2*constantsAndConfigs.stafflineGap)
|
|
bgItem.setZValue(-10) #This is the z value within GuiTrack
|
|
bgItem.setEnabled(False)
|
|
|
|
transparentBlockHandle = GuiBlockHandle(self, block, 0, -2 * constantsAndConfigs.stafflineGap, block["completeDuration"] / constantsAndConfigs.ticksToPixelRatio, h-constantsAndConfigs.stafflineGap) #x, y, w, h
|
|
transparentBlockHandle.color = color
|
|
self.transparentBlockHandles.append(transparentBlockHandle)
|
|
transparentBlockHandle.setPos(block["tickindex"] / constantsAndConfigs.ticksToPixelRatio, -2*constantsAndConfigs.stafflineGap)
|
|
|
|
def createStaffLines(self, lengthInPixel = 0):
|
|
"""By default creates 5 stafflines. But it can be 10;
|
|
5 extra below the origin-staff.
|
|
This is NOT a double-system like a piano but just a staff
|
|
with more lines that happens to have the range of e.g.
|
|
treble + bass clef."""
|
|
|
|
def createLine(yOffset):
|
|
line = QtWidgets.QGraphicsLineItem(QtCore.QLineF(0, 0, lengthInPixel+abs(sceneXOffsetForInitialSigs), 0))
|
|
line.setParentItem(self)
|
|
line.setPen(cosmeticPen)
|
|
self.staffLines.append(line)
|
|
line.setPos(sceneXOffsetForInitialSigs, yOffset*constantsAndConfigs.stafflineGap)
|
|
line.setZValue(-5) #This is the z value within GuiTrack
|
|
|
|
for l in self.staffLines:
|
|
self.parentScore.removeWhenIdle(l)
|
|
self.staffLines = []
|
|
lengthInPixel += 25 #a bonus that gives the hint that you can write after the last object.
|
|
|
|
for i in range(-2, 3): #the normal 5 line system. We have a lower and upper range/position. The middle line is at position 0
|
|
createLine(i)
|
|
|
|
if self.staticExportItem["double"]: #add more stuffs below (user-perspective. positive Qt values)
|
|
for i in range(4, 9): #i is now 3. Make a gap:
|
|
createLine(i)
|
|
|
|
def createBarlines(self, barlinesTickList):
|
|
"""and measure numbers"""
|
|
for bl in self.barLines:
|
|
self.parentScore.removeWhenIdle(bl)
|
|
self.barLines = []
|
|
|
|
if self.staticExportItem["double"]:
|
|
h = 10 * constantsAndConfigs.stafflineGap
|
|
else:
|
|
h = 4 * constantsAndConfigs.stafflineGap
|
|
|
|
#if barlinesTickList[0] == 0: #happens when there is a metrical instruction at tick 0.
|
|
# del barlinesTickList[0]
|
|
|
|
|
|
#Pen for the borders
|
|
pen = QtGui.QPen()
|
|
pen.setWidth(2)
|
|
pen.setColor(QtGui.QColor("black"))
|
|
|
|
|
|
last = None
|
|
offset = 0
|
|
for barnumber, barlineTick in enumerate(barlinesTickList):
|
|
if barlineTick == last:
|
|
offset += 1
|
|
continue #don't draw the double barline
|
|
last = barlineTick
|
|
line = QtWidgets.QGraphicsLineItem(QtCore.QLineF(-3, 0, -3, h)) #x1, y1, x2, y2 #give it a -3 x offset because we have a thickness of 2. Also makes block dividers easier to see.
|
|
line.setPen(pen)
|
|
line.setParentItem(self)
|
|
self.barLines.append(line)
|
|
line.setPos(barlineTick / constantsAndConfigs.ticksToPixelRatio, -2*constantsAndConfigs.stafflineGap)
|
|
|
|
number = QtWidgets.QGraphicsSimpleTextItem(str(barnumber+1-offset))
|
|
number.setScale(0.75)
|
|
number.setParentItem(line)
|
|
number.setPos(-2, -3*constantsAndConfigs.stafflineGap) #-2 on X for a little fine tuning.
|
|
|
|
|
|
|
|
def createBeams(self, beamList):
|
|
"""This creates the beam-rectangle above/below the stems.
|
|
The stems theselves are created in items.py"""
|
|
|
|
for b in self.beams:
|
|
self.parentScore.removeWhenIdle(b)
|
|
self.beams = []
|
|
|
|
for startTick, endTick, beamtype, positionAsStaffline, direction in beamList:
|
|
numberOfBeams = int(log(beamtype, 2)-2)
|
|
assert numberOfBeams == log(beamtype, 2)-2
|
|
for x in range(numberOfBeams):
|
|
|
|
if direction > 0: #stem/beam upwards
|
|
beamNumberOffset = 4*x
|
|
xOffset = 7
|
|
else:
|
|
beamNumberOffset = -4*x
|
|
xOffset = 1
|
|
|
|
|
|
#non-scalable X-Offsets are not possible via setPos. zoom and stretch go haywire.
|
|
#Instead we use one item for streching for the offset position and wrap it in an item group that is used for positioing.
|
|
shifterAnchor = QtWidgets.QGraphicsItemGroup()
|
|
shifterAnchor.setParentItem(self)
|
|
|
|
rectangle = QtWidgets.QGraphicsRectItem(0, 0, (endTick-startTick) / constantsAndConfigs.ticksToPixelRatio, constantsAndConfigs.beamHeight) #x, y, w, h
|
|
rectangle.setBrush(QtGui.QColor("black"))
|
|
|
|
shifterAnchor.addToGroup(rectangle)
|
|
shifterAnchor.rectangle = rectangle
|
|
rectangle.setPos(xOffset, 0)
|
|
|
|
#We need the beam no matter the note head mode.
|
|
if constantsAndConfigs.noteHeadMode:
|
|
rectangle.show()
|
|
else:
|
|
rectangle.hide()
|
|
|
|
self.beams.append(shifterAnchor)
|
|
|
|
x = startTick/ constantsAndConfigs.ticksToPixelRatio
|
|
y = beamNumberOffset + positionAsStaffline * constantsAndConfigs.stafflineGap / 2 - 1
|
|
shifterAnchor.setPos(x, y)
|
|
|
|
class TrackAnchor(QtWidgets.QGraphicsItemGroup):
|
|
"""Handling all items as individuals when deleting a track to redraw it is too much.
|
|
Better let Qt handle it all at once."""
|
|
|
|
def __init__(self, parent):
|
|
super().__init__()
|
|
self.parent = parent
|
|
|
|
def createInitialSignatureArea(self, metaDataDict):
|
|
"""Assumes an empty track, called only in createGraphicItemsFromData
|
|
Initial Signatures, if present. Negative X-axis values.
|
|
|
|
The stafflines draw themselves to the left by the constant sceneXOffsetForInitialSigs.
|
|
|
|
In scoreScene.mouseReleaseEvent we check if the mouse click was within our initial area
|
|
and call the track Editor."""
|
|
|
|
xPos = sceneXOffsetForInitialSigs+5
|
|
clefSVGItem = GuiClef.clefs[metaDataDict["initialClef"]]() #just the graphics
|
|
self.anchor.addToGroup(clefSVGItem)
|
|
clefSVGItem.setPos(xPos, GuiClef.cleffYOnStaff[metaDataDict["initialClef"]])
|
|
|
|
xPos += clefSVGItem.boundingRect().width()
|
|
|
|
initKeySig = GuiKeySignature(metaDataDict["initialKeySignature"])
|
|
self.anchor.addToGroup(initKeySig)
|
|
initKeySig.setPos(xPos, 0)
|
|
xPos += initKeySig.boundingRect().width()
|
|
|
|
#The metrical position is just text. We put that on top of the staff and remove the arrow indicator.
|
|
if metaDataDict["initialMetricalInstruction"]["oneMeasureInTicks"]:
|
|
initMetricalInstruction = GuiMetricalInstruction(metaDataDict["initialMetricalInstruction"], drawMarker=False)
|
|
self.anchor.addToGroup(initMetricalInstruction)
|
|
initMetricalInstruction.setPos(sceneXOffsetForInitialSigs +10, 0) #+10 because the GuiItem itself has a negative x offset
|
|
|
|
|
|
def createGraphicItemsFromData(self, staticRepresentationList):
|
|
"""Create staff objects including simple barlines"""
|
|
self.parentScore.cursor.clearItemHighlight() #or else the current highlight gets deleted while it is on an item
|
|
try:
|
|
self.parentScore.removeWhenIdle(self.anchor)
|
|
except AttributeError: #first round
|
|
pass
|
|
self.items = []
|
|
itemsAppend = self.items.append
|
|
|
|
self.anchor = GuiTrack.TrackAnchor(self)
|
|
self.anchor.setParentItem(self)
|
|
|
|
metaDataDict = staticRepresentationList.pop() #we want the dict, but we also want it separated from the item list. It is, by specs, the last item.
|
|
self.createInitialSignatureArea(metaDataDict)
|
|
|
|
#Real items, positive X-axis values
|
|
for staticItem in staticRepresentationList:
|
|
item = staticItem2Item(staticItem)
|
|
#item.setParentItem(self.anchor)
|
|
self.anchor.addToGroup(item)
|
|
itemsAppend(item)
|
|
item.setPos(item.pixelPosition, 0) # Y axis is set by the position of the track. This sets the position of the whole ItemGroup. The actual pitch of a chord/note is set locally by the chord itself and of no concern here.
|
|
item.setZValue(5) #This is the z value within GuiTrack
|
|
|
|
self.lengthInPixel = metaDataDict["duration"] / constantsAndConfigs.ticksToPixelRatio #this gets updated in stretchXCoordinates
|
|
self.createBarlines(metaDataDict["barlines"])
|
|
self.createBeams(metaDataDict["beams"])
|
|
self.createStaffLines(self.lengthInPixel)
|
|
|
|
|
|
def stretchXCoordinates(self, factor):
|
|
"""Reposition the items on the X axis.
|
|
Call goes through all parents/children, starting from ScoreView._stretchXCoordinates.
|
|
Docstring there."""
|
|
#The rects and lines here are just a QGraphicsRectItem and have no custom subclass. So they have no stretchXCoordinates itself.
|
|
for item in self.items:
|
|
item.setX(item.pos().x() * factor)
|
|
item.stretchXCoordinates(factor)
|
|
|
|
for staticItem in self.staticItems: #labels etc.
|
|
staticItem.setX(staticItem.pos().x() * factor)
|
|
|
|
for barline in self.barLines:
|
|
barline.setX(barline.pos().x() * factor)
|
|
|
|
for staffline in self.staffLines: #stafflines start from x=0. 0*factor=0 so we omit setPos
|
|
stretchLine(staffline, factor)
|
|
|
|
for beam in self.beams: #beams are part of the track, not of the note items.
|
|
beam.setX(beam.pos().x() * factor )
|
|
stretchRect(beam.rectangle, factor)
|
|
|
|
for backgroundBlockColor in self.backgroundBlockColors:
|
|
backgroundBlockColor.setX(backgroundBlockColor.pos().x() * factor)
|
|
stretchRect(backgroundBlockColor, factor)
|
|
|
|
for transparentBlockHandle in self.transparentBlockHandles:
|
|
transparentBlockHandle.setX(transparentBlockHandle.pos().x() * factor)
|
|
transparentBlockHandle.stretchXCoordinates(factor)
|
|
|
|
for ccPath in self.ccPaths.values():
|
|
ccPath.stretchXCoordinates(factor)
|
|
|
|
self.lengthInPixel = self.lengthInPixel * factor #this also gets updated in createGraphicItemsFromData
|
|
|
|
def itemById(self, itemId):
|
|
"""The itemId is the same for backend and gui items. In fact, it is really the
|
|
backend-python-object id. But we exported it as static value."""
|
|
return next(item for item in self.items if item.staticItem["id"] == itemId)
|
|
|
|
def blockAt(self, xScenePosition):
|
|
for th in self.transparentBlockHandles:
|
|
start = th.staticExportItem["tickindex"] / constantsAndConfigs.ticksToPixelRatio
|
|
end = start + th.staticExportItem["completeDuration"] / constantsAndConfigs.ticksToPixelRatio
|
|
if start <= xScenePosition < end:
|
|
return th
|
|
return None #After the last block.
|
|
|
|
def updateMode(self, nameAsString):
|
|
"""Modes are opacity based, not show and hide.
|
|
This gives us the option that children can ignore the opacity (via qt-flag).
|
|
For example we need the block backgrounds to stay visible.
|
|
"""
|
|
assert nameAsString in constantsAndConfigs.availableEditModes
|
|
|
|
#We always hide all CC paths and reactivate only a specific one in cc mode later.
|
|
for ccPath in self.ccPaths.values():
|
|
ccPath.hide()
|
|
self.universalCreateFirstCCBlock.hide()
|
|
|
|
if nameAsString == "notation":
|
|
self.blockModeNameGraphic.hide()
|
|
self.nameGraphic.show()
|
|
self.setOpacity(1)
|
|
for backgroundColor in self.backgroundBlockColors: #created in self.paintBlockBackgroundColors()
|
|
backgroundColor.setOpacity(0.2)
|
|
for tbh in self.transparentBlockHandles:
|
|
tbh.itemMode()
|
|
|
|
elif nameAsString == "cc":
|
|
self.blockModeNameGraphic.hide()
|
|
self.nameGraphic.show()
|
|
self.setOpacity(0.1) #the notes can still be seen in the background.
|
|
if constantsAndConfigs.ccViewValue in self.ccPaths: #It is not guaranteed that all CC Values have content. On the contrary...
|
|
self.ccPaths[constantsAndConfigs.ccViewValue].show()
|
|
else:
|
|
self.universalCreateFirstCCBlock.show()
|
|
for backgroundColor in self.backgroundBlockColors: #created in self.paintBlockBackgroundColors()
|
|
backgroundColor.setOpacity(0.2)
|
|
for tbh in self.transparentBlockHandles:
|
|
tbh.itemMode()
|
|
|
|
|
|
elif nameAsString == "block":
|
|
self.blockModeNameGraphic.show()
|
|
self.nameGraphic.hide()
|
|
self.setOpacity(0) #this hides the notation stuff. The block mode graphics and labels are also children of ours, but they ignore parent opacity.
|
|
for backgroundColor in self.backgroundBlockColors: #simple QRectItems, they don't have their own updateMode function
|
|
backgroundColor.setOpacity(1)
|
|
for tbh in self.transparentBlockHandles:
|
|
tbh.blockMode()
|
|
|