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.
647 lines
30 KiB
647 lines
30 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2019, Nils Hilbricht, Germany ( https://www.hilbricht.net )
|
|
|
|
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
|
|
more specifically its template base application.
|
|
|
|
Laborejo2 is free software: you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation, either version 3 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
"""
|
|
|
|
import logging; logging.info("import {}".format(__file__))
|
|
|
|
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
|
|
|
|
oneRectToReturnThemAll = QtCore.QRectF(0,0,0,0) #prevent the annoying "NotImplementError" from Qt for boundingRect. For items that don't need any collision detection.
|
|
|
|
_zValuesRelativeToConductor = { #Only use for objects added directly to the Conductor, not their children.
|
|
"line":0,
|
|
"startItem":1,
|
|
"block":2,
|
|
"item":4,
|
|
"handle":5,
|
|
}
|
|
|
|
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.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)
|
|
|
|
api.callbacks.updateTempoTrackBlocks.append(self.updateBlocks)
|
|
api.callbacks.updateTempoTrack.append(self.createGraphicItemsFromData)
|
|
api.callbacks.updateTempoTrackMeta.append(self.updateMetaData)
|
|
|
|
def paint(self, *args):
|
|
pass
|
|
def boundingRect(self, *args):
|
|
return oneRectToReturnThemAll
|
|
|
|
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)
|
|
|
|
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")
|
|
|
|
|
|
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):
|
|
|
|
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)
|
|
|
|
|
|
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 event.button() == 1 and 0 <= event.scenePos().x() < self.staffLine.line().x2(): #within the conductor line: # QtCore.Qt.MouseButton.LeftButton
|
|
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
|
|
to create a new tempo point.
|
|
|
|
Uses the values from the item left of it
|
|
"""
|
|
|
|
sp = scenePos.x() * constantsAndConfigs.ticksToPixelRatio
|
|
if constantsAndConfigs.snapToGrid:
|
|
sp = round(sp / constantsAndConfigs.gridRhythm) * constantsAndConfigs.gridRhythm
|
|
|
|
unitsPerMinute, referenceTicks = api.tempoAtTickPosition(sp)
|
|
api.insertTempoItemAtAbsolutePosition(sp, unitsPerMinute, referenceTicks, graphType = "standalone")
|
|
|
|
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.
|
|
|
|
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)
|
|
self.parent = parent #Conductor instance
|
|
self.color = stringToColor(staticExportItem["name"])
|
|
self.trans = QtGui.QColor("transparent")
|
|
self.setPen(self.trans)
|
|
self.setBrush(self.color)
|
|
self.setOpacity(0.2) #mimic background behaviour
|
|
self.staticExportItem = staticExportItem
|
|
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
|
|
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
|
|
|
|
|
|
#def paint(self, *args):
|
|
# """Prevent the selection rectangle when clicking the item"""
|
|
#!! This also prevents the rectangle to show up. Very bad decision.
|
|
# pass
|
|
|
|
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 mouseMoveEvent(self, event):
|
|
"""Don't use the qt system. we move ourselves"""
|
|
event.accept()
|
|
|
|
def mousePressEvent(self, event):
|
|
self.parent.mousePressEvent(event)
|
|
|
|
def mouseMoveEventCustom(self, event):
|
|
"""
|
|
Move the whole block, change the tempoTrack form.
|
|
Custom gets called by the scene mouse press event directly only when the right keys
|
|
are hold down"""
|
|
# All the positions below don't work. They work fine when dragging Tracks around but not this Item. I can't be bothered to figure out why.
|
|
#scenePos() results ins an item position that is translated down and right. The higher the x/y value the more the offset
|
|
#Instead we calculate our delta ourselves.
|
|
|
|
#self.setPos(self.mapToItem(self, event.scenePos()))
|
|
#self.setPos(self.mapFromScene(event.scenePos()))
|
|
#posGlobal = QtGui.QCursor.pos()
|
|
#posView = self.parent.parentScore.parentView.mapFromGlobal(posGlobal) #a widget
|
|
#posScene = self.parent.parentScore.parentView.mapToScene(posView)
|
|
#print (posGlobal, posView, posScene, event.scenePos())
|
|
|
|
|
|
if self.cursorPosOnMoveStart:
|
|
self.setPos(event.scenePos().x(), self.posBeforeMove.y())
|
|
|
|
"""
|
|
#does not work with zooming
|
|
if self.cursorPosOnMoveStart:
|
|
delta = QtGui.QCursor.pos() - self.cursorPosOnMoveStart
|
|
new = self.posBeforeMove + delta
|
|
if new.x() < 0:
|
|
self.setPos(0, self.posBeforeMove.y())
|
|
else:
|
|
self.setPos(new.x(), self.posBeforeMove.y())
|
|
#event.ignore() #this blocks the qt movable object since we already move the object on our own.
|
|
"""
|
|
super().mouseMoveEvent(event)
|
|
|
|
def mousePressEventCustom(self, event):
|
|
"""Custom gets called by the scene mouse press event directly only when the right keys
|
|
are hold down"""
|
|
self.posBeforeMove = self.pos()
|
|
self.cursorPosOnMoveStart = QtGui.QCursor.pos()
|
|
#self.setBrush(self.color)
|
|
super().mousePressEvent(event)
|
|
|
|
def mouseReleaseEventCustom(self, event):
|
|
"""Custom gets called by the scene mouse press event directly only when the right keys
|
|
are hold down"""
|
|
#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 contextMenuEvent(self, event):
|
|
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):
|
|
"""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.
|
|
self.setAcceptHoverEvents(True)
|
|
self.setCursor(QtCore.Qt.SizeHorCursor)
|
|
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
|
|
|
|
pen = QtGui.QPen() # creates a default pen
|
|
#if not self.parentTransparentBlock.staticExportItem["exportsAllItems"]:
|
|
# pen.setStyle(QtCore.Qt.DotLine)
|
|
pen.setWidth(0)
|
|
self.setPen(pen)
|
|
|
|
self.inactivePen = pen
|
|
self.inactivePen.setColor(QtGui.QColor("black"))
|
|
|
|
self.activePen = QtGui.QPen(pen)
|
|
self.activePen.setColor(QtGui.QColor("cyan"))
|
|
|
|
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):
|
|
self.setPen(self.activePen)
|
|
#self.parentTransparentBlock.setBrush(self.parentTransparentBlock.color)
|
|
self.setBrush(QtGui.QColor("cyan"))
|
|
|
|
def hoverLeaveEvent(self, event):
|
|
self.setPen(self.inactivePen)
|
|
#self.parentTransparentBlock.setBrush(self.parentTransparentBlock.trans)
|
|
self.setBrush(QtGui.QColor("black"))
|
|
|
|
def mousePressEvent(self, event):
|
|
self.posBeforeMove = self.pos()
|
|
self.cursorPosOnMoveStart = QtGui.QCursor.pos()
|
|
super().mousePressEvent(event)
|
|
|
|
def mouseMoveEvent(self, event):
|
|
if not self.cursorPosOnMoveStart:
|
|
super().mouseMoveEvent(event)
|
|
return None
|
|
|
|
delta = QtGui.QCursor.pos() - self.cursorPosOnMoveStart
|
|
new = self.posBeforeMove + delta
|
|
|
|
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)
|
|
|
|
event.accept() #this blocks the qt movable object since we already move the object on our own.
|
|
#Don't call the super mouseMoveEvent!
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if self.cursorPosOnMoveStart:
|
|
|
|
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.
|
|
|
|
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
|
|
self.setZValue(_zValuesRelativeToConductor["item"])
|
|
|
|
|
|
self.setAcceptHoverEvents(True)
|
|
|
|
|
|
if not self.staticExportItem["positionInBlock"] == 0:
|
|
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
|
|
|
|
self.note = QtWidgets.QGraphicsTextItem("")
|
|
self.note.setParentItem(self)
|
|
self.note.setFont(constantsAndConfigs.musicFont)
|
|
self.note.setHtml("<font size='6'>{}</font>".format(constantsAndConfigs.realNoteDisplay[staticExportItem["referenceTicks"]]))
|
|
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"]))))
|
|
self.number.setHtml("<font size='2'>{}</font>".format(str(int(staticExportItem["unitsPerMinute"]))))
|
|
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
|
|
|
|
def paint(self, painter, options, widget=None):
|
|
#painter.drawRect(self.boundingRect()) #uncomment to show the bounding rect
|
|
pass
|
|
|
|
def boundingRect(self):
|
|
return QtCore.QRectF(0,0,25,50) #x, y, w, h
|
|
|
|
|
|
def mouseMoveEvent(self, event):
|
|
if self.staticExportItem["positionInBlock"] == 0:
|
|
#First in block can't be moved
|
|
event.accept()
|
|
return
|
|
|
|
#toTheRight = True if event.scenePos().x() - event.lastScenePos().x()) > 0 else False
|
|
|
|
delta = event.scenePos().x() - event.lastScenePos().x()
|
|
newPos = self.x() + delta
|
|
|
|
if 0 <= newPos < self.parentTempoTrack.staffLine.line().x2(): #within the conductor line
|
|
self.setX(newPos)
|
|
event.accept()
|
|
|
|
def mousePressEvent(self, event):
|
|
"""Override the mousePressEvent to deactivate it.
|
|
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
|
|
default value"""
|
|
#if self.staticExportItem["positionInBlock"] == 0:
|
|
# print ("no")
|
|
# #super().mousePressEvent(event)
|
|
event.accept() #
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if self.staticExportItem["positionInBlock"] == 0:
|
|
#First in block can't be moved
|
|
event.accept()
|
|
return
|
|
|
|
tickPositionAbsolute = self.scenePos().x() * constantsAndConfigs.ticksToPixelRatio
|
|
|
|
if constantsAndConfigs.snapToGrid:
|
|
tickPositionAbsolute = round(tickPositionAbsolute / constantsAndConfigs.gridRhythm) * constantsAndConfigs.gridRhythm
|
|
|
|
api.moveTempoItem(self.staticExportItem["id"], tickPositionAbsolute)
|
|
event.accept()
|
|
|
|
def hoverEnterEvent(self, event):
|
|
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"))
|
|
|
|
def hoverLeaveEvent(self, event):
|
|
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"])
|
|
|
|
def wheelEvent(self, event):
|
|
"""This buffers until hoverLeaveEvent and then the new value is sent in self.hoverLeaveEvent"""
|
|
print ("w")
|
|
if event.delta() > 0:
|
|
self.wheelEventChangedValue += 1
|
|
else:
|
|
self.wheelEventChangedValue -= 1
|
|
self.number.setHtml("<font size='2'>{}</font>".format(str(int(self.staticExportItem["unitsPerMinute"] + self.wheelEventChangedValue))))
|
|
event.accept()
|
|
|
|
def keyPressEvent(self, event):
|
|
"""Handle the delete item key.
|
|
Needs grabKeyboard, but NOT setFocus
|
|
|
|
The event will not be sent if it is blocked by a global shortcut.
|
|
"""
|
|
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)
|
|
|
|
def contextMenuEvent(self, event):
|
|
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)
|
|
|
|
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.parent = parent
|
|
self.gridInSeconds = 10
|
|
api.callbacks.updateTempoTrackBlocks.append(self.redraw)
|
|
#no redraw on init. self.parent.staticPoints is not set yet.
|
|
|
|
def paint(self, *args):
|
|
pass
|
|
def boundingRect(self, *args):
|
|
return oneRectToReturnThemAll
|
|
|
|
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"]
|
|
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)
|
|
|
|
|
|
|
|
|
|
|