#! /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 . """ 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("{}".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("{}".format(str(int(staticExportItem["unitsPerMinute"])))) self.number.setHtml("{}".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("{}".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)