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.

635 lines
31 KiB

4 years ago
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
11 months ago
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
4 years ago
4 years ago
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
4 years ago
4 years ago
Laborejo2 is free software: you can redistribute it and/or modify
4 years ago
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")
4 years ago
from PyQt5 import QtCore, QtGui, QtSvg, QtWidgets
from .constantsAndConfigs import constantsAndConfigs
from template.qtgui.helper import stringToColor, removeInstancesFromScene, callContextMenu, stretchLine, stretchRect
from template.helper import pairwise
from .submenus import SecondaryTempoChangeMenu, TempoBlockPropertiesEdit
import engine.api as api
_zValuesRelativeToConductor = { #Only use for objects added directly to the Conductor, not their children.
4 years ago
"line":0,
"startItem":1,
"block":2,
"item":4,
"handle":5,
4 years ago
}
class Conductor(QtWidgets.QGraphicsItem):
"""The track for tempo items. Some methods have the same name and functionality
as note-track to be compatible. For example drag and drop of blocks."""
def __init__(self, parentView):
super().__init__()
self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden
4 years ago
self.parentView = parentView
self.totalHeight = 71
self.staticPoints = None #Cached backend staticRepresentationList: TempoPoints and interpolated points list
self.staticBlocks = None #Cached Block Data list
self.staticMeta = None #Cached track meta data dict.
self.staffLine = QtWidgets.QGraphicsLineItem(0,0,10,0) #x1, y1, x2, y2
self.staffLine.setParentItem(self)
self.staffLine.setPos(0,0)
#Displays the real time in minutes and seconds. Definition at the end of thils file.
self.timeLine = TimeLine(self) #registers its own callbacks
self.timeLine.setParentItem(self)
4 years ago
api.callbacks.updateTempoTrackBlocks.append(self.updateBlocks)
api.callbacks.updateTempoTrack.append(self.createGraphicItemsFromData)
api.callbacks.updateTempoTrackMeta.append(self.updateMetaData)
def boundingRect(self):
"""The tempo track extends above the real tracks"""
return QtCore.QRectF(0, -1*self.totalHeight, 20000, 1.2*self.totalHeight) #x,y,w,h
4 years ago
def blockAt(self, xScenePosition):
for block in ConductorTransparentBlock.instances:
start = block.staticExportItem["position"] / constantsAndConfigs.ticksToPixelRatio
end = start + block.staticExportItem["duration"] / constantsAndConfigs.ticksToPixelRatio
if start <= xScenePosition < end:
return block
return None #After the last block.
@property
def transparentBlockHandles(self):
return ConductorTransparentBlock.instances
def updateMetaData(self, trackMetaDictionary):
"""Keep the meta data up to date.
Meta Data is (absolute) min and max tempo values:
{'minimumAbsoluteTempoValue': 120, 'maximumAbsoluteTempoValue': 120}
"""
self.staticMeta = trackMetaDictionary
def updateBlocks(self, staticRepresentationList):
"""This is called when the blocks itself change, of course.
But also """
self.staticBlocks = staticRepresentationList
removeInstancesFromScene(ConductorTransparentBlock)
for dictExportItem in staticRepresentationList:
guiBlock = ConductorTransparentBlock(parent = self, staticExportItem = dictExportItem, x = 0, y = -10, w = dictExportItem["duration"] / constantsAndConfigs.ticksToPixelRatio, h = 20)
guiBlock.setParentItem(self)
guiBlock.setPos(dictExportItem["position"] / constantsAndConfigs.ticksToPixelRatio,0)
4 years ago
rightBorder = (dictExportItem["duration"] + dictExportItem["position"]) / constantsAndConfigs.ticksToPixelRatio
self.updateStaffLine(rightBorder)
def blockById(self, backendId):
for guiblock in ConductorTransparentBlock.instances:
if guiblock.staticExportItem["id"] == backendId:
return guiblock
#else:
# raise ValueError(f"{backendId} not found")
4 years ago
def updateStaffLine(self, x):
assert not self.staffLine.line().isNull()
line = self.staffLine.line()
line.setLength(x)
self.staffLine.setLine(line)
self.staffLine.setZValue(_zValuesRelativeToConductor["line"])
def createGraphicItemsFromData(self, staticRepresentationList):
4 years ago
self.staticPoints = staticRepresentationList
removeInstancesFromScene(TempoPoint)
y = -35 #The Y Value adjusts for the offset the text-item creates
for point in staticRepresentationList:
if not point["type"] == "interpolated": #a real user point or lastInBlock or lastInTrack
x = point["position"] / constantsAndConfigs.ticksToPixelRatio
p = TempoPoint(self, point, self.blockById(point["blockId"]))
p.setParentItem(self)
p.setPos(x, y)
4 years ago
def stretchXCoordinates(self, factor):
"""Reposition the items on the X axis.
Call goes through all parents/children, starting from ScoreView._stretchXCoordinates.
Docstring there."""
positionalItems = (TempoPoint.instances + ConductorTransparentBlock.instances)
for tempoPoint in positionalItems:
tempoPoint.setX(tempoPoint.pos().x() * factor)
for block in ConductorTransparentBlock.instances:
block.stretchXCoordinates(factor)
stretchLine(self.staffLine, factor)
self.timeLine.stretchXCoordinates(factor)
def mousePressEvent(self, event):
"""It is possible that this has coordinates outside of the Conductor instance. When the
mousePressEvent is inside and the mouse moves outside for the release event it still counts
as event of this instance"""
if self.parentView.mode() in ("notation", "cc") and event.button() == QtCore.Qt.LeftButton and 0 <= event.scenePos().x() < self.staffLine.line().x2(): #within the conductor line.
4 years ago
event.accept()
self.add(event.scenePos()) #create a new tempo point by telling the api a position and then reacting to "delete all, recreate" from the callback.
else:
super().mousePressEvent(event) #call default implementation from QGraphicsRectItem
def add(self, scenePos):
"""Use a scenePos (from self.mousePressEvent) to instruct the backend
4 years ago
to create a new tempo point.
Uses the values from the item left of it
4 years ago
"""
4 years ago
sp = scenePos.x() * constantsAndConfigs.ticksToPixelRatio
if constantsAndConfigs.snapToGrid:
sp = round(sp / constantsAndConfigs.gridRhythm) * constantsAndConfigs.gridRhythm
4 years ago
unitsPerMinute, referenceTicks = api.tempoAtTickPosition(sp)
api.insertTempoItemAtAbsolutePosition(sp, unitsPerMinute, referenceTicks, graphType = "standalone", description="")
4 years ago
class ConductorTransparentBlock(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.
4 years ago
The block handle is at the END of a block. """
class ConductorBlockName(QtWidgets.QGraphicsSimpleTextItem):
instances = []
def __init__(self, parent, positionInSeconds):
self.__class__.instances.append(self)
m, s = divmod(positionInSeconds, 60)
text = "{}:{} min".format(str(int(m)).zfill(2), str(int(s)).zfill(2))
super().__init__(text)
marker = QtWidgets.QGraphicsLineItem(0, 0, 0, -10) #vertical marker to connect to the conductor line
marker.setParentItem(self)
instances = []
def __init__(self, parent, staticExportItem, x, y, w, h):
self.__class__.instances.append(self)
super().__init__(x, y, w, h)
self.setFlags(QtWidgets.QGraphicsItem.ItemDoesntPropagateOpacityToChildren|QtWidgets.QGraphicsItem.ItemIsMovable) #no mouseReleaseEvent without selection or movable.
self.setAcceptHoverEvents(True)
4 years ago
self.parent = parent #Conductor instance
2 years ago
self.parentGuiTrack = parent #redundant, but specifically for block movement. see ScoreScene
4 years ago
self.color = stringToColor(staticExportItem["name"])
self.trans = QtGui.QColor("transparent")
self.setPen(self.trans)
self.setBrush(self.color)
self.setOpacity(0.5) #mimic background behaviour
self.staticExportItem = staticExportItem
4 years ago
self.posBeforeMove = None
self.cursorPosOnMoveStart = None
#Display Block ID
#self.idText = QtWidgets.QGraphicsSimpleTextItem(str(self.staticExportItem["id"]))
#self.idText.setParentItem(self)
#self.idText.setPos(0, -30)
if self.staticExportItem["duration"] >= 8*api.D1: #cosmetics
self.startLabel = QtWidgets.QGraphicsSimpleTextItem(self.staticExportItem["name"] + " start")
self.startLabel.setParentItem(self)
self.startLabel.setPos(15, -2*constantsAndConfigs.stafflineGap)
self.startLabel.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
self.endLabel = QtWidgets.QGraphicsSimpleTextItem(self.staticExportItem["name"] + " end ")
self.endLabel.setParentItem(self)
self.endLabel.setPos(self.rect().width() - self.endLabel.boundingRect().width(), -2*constantsAndConfigs.stafflineGap)
self.endLabel.setFlags(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
else:
self.startLabel = QtWidgets.QGraphicsSimpleTextItem("")
self.endLabel = QtWidgets.QGraphicsSimpleTextItem("")
#Add Resizing Handle at the end
4 years ago
self.marker = ConductorBlockHandle(parent = self)
self.marker.setParentItem(self)
self.marker.setPos(staticExportItem["duration"] / constantsAndConfigs.ticksToPixelRatio, -1/2* self.rect().height()+2)
#self.setZValue(_zValuesRelativeToConductor["handle"]) #includes the handle
4 years ago
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.marker.setX(self.marker.pos().x() * factor)
def mousePressEvent(self, event):
"""Just reroute to the parent score so that mousePressEventCustom can get triggered"""
4 years ago
self.parent.mousePressEvent(event)
def mousePressEventCustom(self, event):
"""Custom gets called by the scene mouse press event"""
4 years ago
self.posBeforeMove = self.pos()
self.cursorPosOnMoveStart = QtGui.QCursor.pos()
#self.setBrush(self.color)
super().mousePressEvent(event)
def mouseReleaseEventCustom(self, event):
#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. If the moving was correct then the new position will be set by redrawing the whole Conductor.
self.posBeforeMove = None
self.cursorPosOnMoveStart = None
super().mouseReleaseEvent(event)
def splitHere(self, event):
posRelativeToBlockStart = event.scenePos().x() * constantsAndConfigs.ticksToPixelRatio - self.x() * constantsAndConfigs.ticksToPixelRatio
if constantsAndConfigs.snapToGrid:
posRelativeToBlockStart = round(posRelativeToBlockStart / constantsAndConfigs.gridRhythm) * constantsAndConfigs.gridRhythm
if posRelativeToBlockStart > 0:
api.splitTempoBlock(self.staticExportItem["id"], int(posRelativeToBlockStart))
def contextMenuEventCustom(self, event):
4 years ago
listOfLabelsAndFunctions = [
("edit properties", lambda: TempoBlockPropertiesEdit(self.scene().parentView.mainWindow, staticExportItem = self.staticExportItem)),
("separator", None),
("split here", lambda: self.splitHere(event)),
("duplicate", lambda: api.duplicateTempoBlock(self.staticExportItem["id"])),
("create content link", lambda: api.duplicateContentLinkTempoBlock(self.staticExportItem["id"])),
("unlink", lambda: api.unlinkTempoBlock(self.staticExportItem["id"])),
("separator", None),
("join with next block", lambda: api.mergeWithNextTempoBlock(self.staticExportItem["id"])),
("delete block", lambda: api.deleteTempoBlock(self.staticExportItem["id"])),
("separator", None),
("append block at the end", api.appendTempoBlock),
]
callContextMenu(listOfLabelsAndFunctions)
class ConductorBlockHandle(QtWidgets.QGraphicsRectItem):
4 years ago
"""Provides user interaction so the temp block can be resized by moving this handle with the
mouse left and right.
When user interaction happens this handle acts upon its parent transparent block to resize it
and finally sends a message to the backend, to ask for a data change."""
def __init__(self, parent):
self.parentTransparentBlock = parent
#Line item super().__init__(0,-1, 0, parent.rect().height()-4) #x1, y1, x2, y2
super().__init__(-3, -2, 3, parent.rect().height()) #x, y, w, h
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable|QtWidgets.QGraphicsItem.ItemSendsGeometryChanges|QtWidgets.QGraphicsItem.ItemIsFocusable|QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity) #QtWidgets.QGraphicsItem.ItemClipsToShape puts the item behind the parent rect and only receives event inside its own shape.
4 years ago
self.setAcceptHoverEvents(True)
self.setZValue(_zValuesRelativeToConductor["handle"]) #The handle does not compete with the transparent block but with its contents! Therefore the startItem is set to have a custom lower priority than the handle
self.setBrush(QtGui.QColor("black"))
self.minimalSize = api.D1 / constantsAndConfigs.ticksToPixelRatio
4 years ago
pen = QtGui.QPen() # creates a default pen
#if not self.parentTransparentBlock.staticExportItem["exportsAllItems"]:
# pen.setStyle(QtCore.Qt.DotLine)
pen.setWidth(0)
4 years ago
self.setPen(pen)
self.inactivePen = pen
self.inactivePen.setColor(QtGui.QColor("black"))
self.activePen = QtGui.QPen(pen)
self.activePen.setColor(QtGui.QColor("cyan"))
4 years ago
def shape(self):
"""Return a more accurate shape for this item so that
mouse hovering is more accurate"""
path = QtGui.QPainterPath()
path.addRect(QtCore.QRectF(-2, -2, 5, self.parentTransparentBlock.rect().height()+2 )) #this is directly related to inits parameter x, y, w, h
return path
def hoverEnterEvent(self, event):
if self.parentTransparentBlock.parent.parentView.mode() in ("notation", "cc"): #Blockmode is for moving around. not resizing.
self.setCursor(QtCore.Qt.SizeHorCursor)
self.setPen(self.activePen)
#self.parentTransparentBlock.setBrush(self.parentTransparentBlock.color)
self.setBrush(QtGui.QColor("cyan"))
4 years ago
def hoverLeaveEvent(self, event):
if self.parentTransparentBlock.parent.parentView.mode() in ("notation", "cc"): #Blockmode is for moving around. not resizing.
self.unsetCursor()
self.setPen(self.inactivePen)
#self.parentTransparentBlock.setBrush(self.parentTransparentBlock.trans)
self.setBrush(QtGui.QColor("black"))
4 years ago
def mousePressEvent(self, event):
if self.parentTransparentBlock.parent.parentView.mode() in ("notation", "cc"): #Blockmode is for moving around. not resizing.
self.posBeforeMove = self.pos()
self.cursorPosOnMoveStart = QtGui.QCursor.pos()
super().mousePressEvent(event)
4 years ago
def mouseMoveEvent(self, event):
if self.parentTransparentBlock.parent.parentView.mode() in ("notation", "cc"): #Blockmode is for moving around. not resizing.
if not self.cursorPosOnMoveStart:
super().mouseMoveEvent(event)
return None
4 years ago
delta = QtGui.QCursor.pos() - self.cursorPosOnMoveStart
new = self.posBeforeMove + delta
4 years ago
if not new.x() < self.minimalSize:
self.setPos(new.x(), self.posBeforeMove.y())
pRect = self.parentTransparentBlock.rect()
pRect.setRight(new.x())
self.parentTransparentBlock.setRect(pRect)
4 years ago
event.accept() #this blocks the qt movable object since we already move the object on our own.
#Don't call the super mouseMoveEvent!
4 years ago
def mouseReleaseEvent(self, event):
if self.cursorPosOnMoveStart and self.parentTransparentBlock.parent.parentView.mode() in ("notation", "cc"):
4 years ago
endingRelativeToBlockStart = self.x() * constantsAndConfigs.ticksToPixelRatio - self.parentTransparentBlock.x()
if constantsAndConfigs.snapToGrid:
endingRelativeToBlockStart = round(endingRelativeToBlockStart / constantsAndConfigs.gridRhythm) * constantsAndConfigs.gridRhythm
assert endingRelativeToBlockStart > 0
self.setPos(self.posBeforeMove) #In case the handle was moved to a position where it wasn't allowed no backend action will happen. Just in case we reset the graphics for a smoother user experience.
self.posBeforeMove = None
self.cursorPosOnMoveStart = None
api.changeTempoBlockDuration(self.parentTransparentBlock.staticExportItem["id"], endingRelativeToBlockStart)
super().mouseReleaseEvent(event)
class TempoPoint(QtWidgets.QGraphicsItem):
"""A point where the values can be edited by the user.
Only in notation and cc mode. Blockmode is for moving the blocks itself.
4 years ago
The first TempoPoint cannot be hovered. It is instead attached to a block handle.
"""
instances = []
def __init__(self, parentTempoTrack, staticExportItem, parentBlock):
self.__class__.instances.append(self)
super().__init__()
self.staticExportItem = staticExportItem
self.parentTempoTrack = parentTempoTrack
self.parentBlock = parentBlock
4 years ago
self.setZValue(_zValuesRelativeToConductor["item"])
self.setAcceptHoverEvents(True)
if not self.staticExportItem["positionInBlock"] == 0:
4 years ago
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable|QtWidgets.QGraphicsItem.ItemIsFocusable)
#Too irritating. And confuses with handle movement. self.setCursor(QtCore.Qt.SizeHorCursor) #this sets the cursor while the mouse is over the item. It is independent of AcceptHoverEvents
else:
self.setZValue(0) #avoid hovering conflict with block handle
self.ungrabMouse = api.nothing #to surpress a warning from the context menu
4 years ago
#Set this flag after setFlags above
self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden
4 years ago
self.note = QtWidgets.QGraphicsTextItem("")
self.note.setParentItem(self)
self.note.setFont(constantsAndConfigs.musicFont)
s = constantsAndConfigs.realNoteDisplay[staticExportItem["referenceTicks"]]
self.note.setHtml(f"<font size='6'>{s}</font>")
4 years ago
self.note.setPos(-6,0) #adjust items font x offsset.
self.number = QtWidgets.QGraphicsTextItem("")
self.number.setParentItem(self)
#self.number.setHtml("<font color='black' size='2'>{}</font>".format(str(int(staticExportItem["unitsPerMinute"]))))
if staticExportItem["lilypondParameters"]["tempo"]:
numberString = f"""{staticExportItem["lilypondParameters"]["tempo"]}({int(staticExportItem["unitsPerMinute"])})"""
else:
numberString = str(int(staticExportItem["unitsPerMinute"]))
#self.number.setHtml("<font size='2'>{}</font>".format(str(int(staticExportItem["unitsPerMinute"]))))
self.number.setHtml(f"<font size='2'>{numberString}</font>")
4 years ago
self.number.setPos(-6,0) #adjust items font x offsset.
if not self.staticExportItem["graphType"] == "standalone":
self.arrow = QtWidgets.QGraphicsTextItem("") #unicode long arrow right #http://xahlee.info/comp/unicode_arrows.html
self.arrow.setParentItem(self)
self.arrow.setPos(7,30)
else:
self.arrow = None
for n in (self.note, self.number, self.arrow):
if n: n.setDefaultTextColor(QtGui.QColor("black"))
self.wheelEventChangedValue = 0 #resetted in hoverEnterEvent. But we still need it for new items that appear directly under the mouse cursor
4 years ago
def boundingRect(self):
"""We could return self.childrenBoundingRect() but we want the clickable area to be bigger"""
4 years ago
return QtCore.QRectF(0,0,25,50) #x, y, w, h
def mouseMoveEvent(self, event):
if self.parentTempoTrack.parentView.mode() in ("notation", "cc"):
if self.staticExportItem["positionInBlock"] == 0:
#First in block can't be moved
event.accept()
return
4 years ago
#toTheRight = True if event.scenePos().x() - event.lastScenePos().x()) > 0 else False
4 years ago
delta = event.scenePos().x() - event.lastScenePos().x()
newPos = self.x() + delta
4 years ago
if 0 <= newPos < self.parentTempoTrack.staffLine.line().x2(): #within the conductor line
self.setX(newPos)
event.accept()
else:
event.accept()
4 years ago
def mousePressEvent(self, event):
"""Override the mousePressEvent to deactivate it.
4 years ago
Otherwise the event will be sent to the parent block and create a new TempoItem
at this point even if there is already one. Effectively replacing a custom item with
4 years ago
default value"""
#if self.staticExportItem["positionInBlock"] == 0:
# print ("no")
# #super().mousePressEvent(event)
4 years ago
event.accept() #
def mouseReleaseEvent(self, event):
if self.parentTempoTrack.parentView.mode() in ("notation", "cc"):
if self.staticExportItem["positionInBlock"] == 0:
#First in block can't be moved
event.accept()
return
tickPositionAbsolute = self.scenePos().x() * constantsAndConfigs.ticksToPixelRatio
4 years ago
if constantsAndConfigs.snapToGrid:
tickPositionAbsolute = round(tickPositionAbsolute / constantsAndConfigs.gridRhythm) * constantsAndConfigs.gridRhythm
4 years ago
api.moveTempoItem(self.staticExportItem["id"], tickPositionAbsolute)
event.accept()
else:
event.accept()
def hoverEnterEvent(self, event):
if self.parentTempoTrack.parentView.mode() in ("notation", "cc"):
self.parentTempoTrack.parentView.mainWindow.ui.actionDelete.setEnabled(False) #Delete key collides with our hover-delete.
self.grabKeyboard()
self.wheelEventChangedValue = 0
for n in (self.note, self.number, self.arrow):
if n: n.setDefaultTextColor(QtGui.QColor("cyan"))
else:
event.accept()
4 years ago
def hoverLeaveEvent(self, event):
if self.parentTempoTrack.parentView.mode() in ("notation", "cc"):
self.parentTempoTrack.parentView.mainWindow.ui.actionDelete.setEnabled(True) #Delete key collides with our hover-delete.
self.ungrabKeyboard()
for n in (self.note, self.number, self.arrow):
if n: n.setDefaultTextColor(QtGui.QColor("black"))
if self.wheelEventChangedValue:
api.insertTempoItemAtAbsolutePosition(self.staticExportItem["position"], self.staticExportItem["unitsPerMinute"] + self.wheelEventChangedValue, self.staticExportItem["referenceTicks"], self.staticExportItem["graphType"], self.staticExportItem["lilypondParameters"]["tempo"], )
else:
event.accept()
4 years ago
def wheelEvent(self, event):
"""This buffers until hoverLeaveEvent and then the new value is sent in self.hoverLeaveEvent"""
if self.parentTempoTrack.parentView.mode() in ("notation", "cc"):
if event.delta() > 0:
self.wheelEventChangedValue += 1
else:
self.wheelEventChangedValue -= 1
if self.staticExportItem["lilypondParameters"]["tempo"]:
numberString = f"""{self.staticExportItem["lilypondParameters"]["tempo"]}({int(self.staticExportItem["unitsPerMinute"]) + self.wheelEventChangedValue})"""
else:
numberString = str(int(self.staticExportItem["unitsPerMinute"]) + self.wheelEventChangedValue )
#self.number.setHtml("<font size='2'>{}</font>".format(str(int(self.staticExportItem["unitsPerMinute"] + self.wheelEventChangedValue))))
self.number.setHtml(f"<font size='2'>{numberString}</font>")
event.accept()
4 years ago
else:
event.accept()
4 years ago
def keyPressEvent(self, event):
"""Handle the delete item key.
Needs grabKeyboard, but NOT setFocus
4 years ago
The event will not be sent if it is blocked by a global shortcut.
"""
if self.parentTempoTrack.parentView.mode() in ("notation", "cc"):
key = event.key()
if key == 16777223:
#after delete the item and tempo tracks gets recreated so we need to reactivate the shortcut now. It will work without these two lines, but that is only implicit behaviour.
self.parentTempoTrack.parentView.mainWindow.ui.actionDelete.setEnabled(True) #Delete key collides with our hover-delete.
self.ungrabKeyboard()
api.removeTempoItem(self.staticExportItem["id"])
else:
return super().keyPressEvent(event)
4 years ago
else:
event.accept()
4 years ago
def contextMenuEvent(self, event):
if not self.parentTempoTrack: return #there was a rare bug here. don't let it crash
if self.parentTempoTrack.parentView.mode() in ("notation", "cc"):
listOfLabelsAndFunctions = [
("edit properties", lambda: SecondaryTempoChangeMenu(self.scene().parentView.mainWindow, staticExportTempoItem = self.staticExportItem)),
]
if not self.staticExportItem["positionInBlock"] == 0:
listOfLabelsAndFunctions.append(("delete", lambda: api.removeTempoItem(self.staticExportItem["id"])))
callContextMenu(listOfLabelsAndFunctions)
else:
event.accept()
4 years ago
class TimeLine(QtWidgets.QGraphicsItem):
"""Displays the real time."""
class TimePoint(QtWidgets.QGraphicsSimpleTextItem):
instances = []
def __init__(self, parent, positionInSeconds):
self.__class__.instances.append(self)
m, s = divmod(positionInSeconds, 60)
text = "{}:{} min".format(str(int(m)).zfill(2), str(int(s)).zfill(2))
super().__init__(text)
marker = QtWidgets.QGraphicsLineItem(0, 0, 0, -10) #vertical marker to connect to the conductor line
marker.setParentItem(self)
def __init__(self, parent):
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.parent = parent #
4 years ago
self.gridInSeconds = 10
api.callbacks.updateTempoTrackBlocks.append(self.redraw)
#no redraw on init. self.parent.staticPoints is not set yet.
def boundingRect(self):
return self.parent.boundingRect()
4 years ago
def redraw(self, staticRepresentationList):
if not self.parent.staticPoints:
return None
removeInstancesFromScene(self.TimePoint)
sliceStartInSeconds = 0 #counted, not calculated
gridCounter = 0 # int("how often was a gridMarker generated")
result = []
for nowPoint, nextPoint in pairwise(self.parent.staticPoints):
"""Values we can't calculate:
sliceEndInTicks / ticksPerSecond . sliceEndInTicks is an absolute position.
but ticksPerSecond changes with every slice. Therefore you would discard all
ticksPerSecond value except the last and calculate a wrong value.
Instead we need to calculate the seconds always in the slice and then count
the overall length.
"""
tempoForThisSlice = abs(nowPoint["value"])
sliceDurationInTicks = nextPoint["position"] - nowPoint["position"]
ticksPerSecondForThisSlice = tempoForThisSlice * api.D4 / 60 # ["value"] is normalized beatsPerMinute(!) for quarter notes. calculated from "units" and "referenceTicks"
sliceDurationInSeconds = sliceDurationInTicks / ticksPerSecondForThisSlice
sliceEndInSeconds = sliceStartInSeconds + sliceDurationInSeconds
while sliceStartInSeconds < (gridCounter+1) * self.gridInSeconds <= sliceEndInSeconds: #is the next grid marker(+1) in the current slice?
gridCounter += 1
secondsSinceLastTempoChange = gridCounter * self.gridInSeconds - sliceStartInSeconds
posInTicks = nowPoint["position"] + secondsSinceLastTempoChange * ticksPerSecondForThisSlice
assert nowPoint["position"] <= posInTicks <= nextPoint["position"], (nowPoint["position"], posInTicks, nextPoint["position"])
4 years ago
result.append((posInTicks, gridCounter * self.gridInSeconds))
sliceStartInSeconds = sliceEndInSeconds
#After the loop both sliceStartInSeconds and sliceEndInSeconds are equal to the overall length of the track in seconds
#Add an end marker to the results and create the qGraphicItems to display the time markers.
if nextPoint["position"] > 0:
result.append((nextPoint["position"], sliceEndInSeconds))
for tickPos, secPos in result:
timePoint = self.TimePoint(self, secPos)
timePoint.setParentItem(self)
timePoint.setPos(tickPos / constantsAndConfigs.ticksToPixelRatio, 10)
def stretchXCoordinates(self, factor):
"""Does NOT just reposition the existing items but displays a different
time grid"""
for timePoint in self.TimePoint.instances:
timePoint.setX(timePoint.pos().x() * factor)