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.
 
 

604 lines
28 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")
from PyQt5 import QtCore, QtGui, QtSvg, QtWidgets
translate = QtCore.QCoreApplication.translate
from .constantsAndConfigs import constantsAndConfigs
from template.qtgui.helper import stringToColor, removeInstancesFromScene, callContextMenu, stretchLine, stretchRect
import engine.api as api
class CCPath(QtWidgets.QGraphicsRectItem):
"""
A CCPath only exists when the backend track has a cc-part activated.
Since each track has 128 potential CC paths we don't create and
save a bunch of empty ones, neither in the backend nor here.
Therefore: you cannot display a message "click here to create
a CC track/block" here in the CC Path since the CCPath does not
exist at this point in time. Thus this button is created in the track. One button for all CCs.
There are two item types. Movable, or handle-points,
and non-movable: interpolation-points.
The backend only knows handle points and a function which
interpolates until the next point.
However, that results only in CC values 0-128 anyway. So
we use these directly.
We don't lie to the user. There is no smooth curve, not even
a linear one, between CC values 65 and 66. It is just a step.
So we show the steps."""
def __init__(self, parentGuiTrack, parentDataTrackId):
super().__init__(0, 0, 0, 0)
self.setAcceptHoverEvents(True)
#self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable|QtWidgets.QGraphicsItem.ItemSendsGeometryChanges|QtWidgets.QGraphicsItem.ItemIsFocusable|QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
self.setFlags(QtWidgets.QGraphicsItem.ItemIsFocusable|QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
#self.setBrush(QtCore.Qt.red) #debug
self.setPen(QtGui.QPen(QtCore.Qt.transparent))
self.parentDataTrackId = parentDataTrackId
self.parentGuiTrack = parentGuiTrack
self.hoveringOverItem = None #user point or block end marker. Used for scoreScene mousePressEvent detection
self.userItems = []
self.interpolatedItems = []
self.other = []
self.blockListCache = [] #backend dictExport items. updated through self.updateGraphBlockTrack
self.blockdictCache = {} #blockId:guiBlock updated through self.updateGraphBlockTrack
self.transparentBlockHandles = [] #transparentBlockHandles in correct order. updated through self.updateGraphBlockTrack
def itemChange(self, changeEnum, value):
if changeEnum == QtWidgets.QGraphicsItem.ItemVisibleHasChanged: #12
if self.isVisible():
self.parentGuiTrack.universalCreateFirstCCBlock.hide()
else:
self.parentGuiTrack.universalCreateFirstCCBlock.show()
return super().itemChange(changeEnum, value)
def mousePressEventToAdd(self, event):
"""Called by scorescene mousePressEvent"""
if event.button() == 1: #QtCore.Qt.LeftButton
self.add(event)
@property
def items(self):
return self.userItems + self.interpolatedItems + self.other
#@property
#def transparentBlockHandles(self):
#return CCGraphTransparentBlock.instances This does not work because we need the blocks per track, not all of them in all tracks.
def createGraphicItemsFromData(self, staticRepresentationList):
"""The tick position is, as always, a long tickvalue from the
backend.
The CC value, 0-127, has its default 0-line on the middle line
as all items, since this is the track root. Since CC 64 is the
middle value for CCs we shift all the points down by 64 so
the middle staff line becomes CC 64.
"""
for item in self.items:
self.scene().removeWhenIdle(item)
self.other = []
self.userItems = []
self.interpolatedItems = []
for point in staticRepresentationList:
if point["type"] == "interpolated":
p = CCInterpolatedPoint()
self.interpolatedItems.append(p)
self.userItems[-1].interpolatedItemsRight.append(p)
else: #user, lastInBlock or lastInTrack
p = CCUserPoint(self, point)
if self.userItems:
p.interpolatedItemsLeft = self.userItems[-1].interpolatedItemsRight
self.userItems.append(p)
t = QtWidgets.QGraphicsSimpleTextItem(str(abs(point["value"])))
#t.setFont(constantsAndConfigs.theFont)
t.setParentItem(self)
t.setScale(0.75)
self.other.append(t)
t.setPos(point["position"] / constantsAndConfigs.ticksToPixelRatio, 29)
p.setParentItem(self)
p.setPos(point["position"] / constantsAndConfigs.ticksToPixelRatio , (point["value"]+64)*0.4375) #value goes from -0 to -127 and are compressed to fit between two lines in the staff.
def updateGraphBlockTrack(self, staticRepresentationList):
"""Handles and visualizes block boundaries"""
#{"type" : "GraphBlock", "id":id(block), "name":block.name, "duration":block.duration, "position":tickCounter}
self.blockListCache = staticRepresentationList #sorted list
self.blockdictCache = {} #reset
for tbh in self.transparentBlockHandles:
tbh.setParentItem(None)
tbh.scene().removeWhenIdle(tbh)
self.transparentBlockHandles = [] #reset
self.hoveringOverItem = None #because moving an item, triggering delete and recreation, will never hoverOutEvent
#removeInstancesFromScene(CCGraphTransparentBlock) #this removes ALL blocks in all tracks from the scene. don't.
for dictExportItem in staticRepresentationList:
guiBlock = CCGraphTransparentBlock(parent = self, staticExportItem = dictExportItem, x = 0, y = -28, w = dictExportItem["duration"] / constantsAndConfigs.ticksToPixelRatio, h = 2*28)
guiBlock.setParentItem(self)
guiBlock.setPos(dictExportItem["position"] / constantsAndConfigs.ticksToPixelRatio,0)
self.blockdictCache[dictExportItem["id"]] = guiBlock
self.transparentBlockHandles.append(guiBlock)
#Use the last item of the loop above to draw length of the boundary lines.
maximumPixels = (dictExportItem["duration"] + dictExportItem["position"]) / constantsAndConfigs.ticksToPixelRatio
upperLine = QtWidgets.QGraphicsLineItem(0,-28,maximumPixels,-28) #x1, y1, x2, y2
#upperLine.setPen(QtGui.QColor("red"))
upperLine.setParentItem(self)
self.other.append(upperLine)
lowerLine = QtWidgets.QGraphicsLineItem(0,28,maximumPixels,28) #x1, y1, x2, y2
#lowerLine.setPen(QtGui.QColor("green"))
lowerLine.setParentItem(self)
self.other.append(lowerLine)
upperLine.setOpacity(0.3)
lowerLine.setOpacity(0.3)
upperLine.setPos(0,0)
lowerLine.setPos(0,0)
#for self.stretchXCoordinates
self.upperLine = upperLine
self.lowerLine = lowerLine
self.setRect(0,-28,maximumPixels,2*28)
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)
stretchLine(self.upperLine, factor)
stretchLine(self.lowerLine, factor)
for item in self.items:
item.setX(item.pos().x() * factor)
for transparentBlockHandle in self.transparentBlockHandles:
transparentBlockHandle.setX(transparentBlockHandle.pos().x() * factor)
transparentBlockHandle.stretchXCoordinates(factor)
def blocKByPosition(self, tickPositionAbsoluteBackendValue):
for block in self.blockListCache:
start = block["position"]
end = start + block["duration"]
if start <= tickPositionAbsoluteBackendValue < end:
return block["id"], block["position"]
return None #After the last block.
def blockAt(self, qScenePosition):
"""For compatibility with track.blockAt
Can return a block or None for "after the last block" """
backendPosition = qScenePosition * constantsAndConfigs.ticksToPixelRatio
result = self.blocKByPosition(backendPosition)
if result:
blockId, blockBackendPosition = result
assert blockId in self.blockdictCache
return self.blockdictCache[blockId]
else:
return None
def getCCNumber(self):
"""The CC number is a dynamic value"""
for ccNumber, ccGraphTrack in self.parentGuiTrack.ccPaths.items():
if ccGraphTrack is self:
return ccNumber
else:
raise ValueError("This Block is not in the ccPath dict of the GuiTrack""")
def getYAsBackendValue(self, y):
newCCValue = int(y / 0.4375 - 64) #value goes from -0 to -127 and are compressed to fit between two lines in the staff.
newCCValue = abs(newCCValue)
assert 0 <= newCCValue < 128
return newCCValue
def add(self, event):
"""Only activated through the hover area which gives are the
most control over where clicks are allowed.
"""
pos = self.mapFromScene(event.scenePos())
x = pos.x()
y = pos.y()
if -28 < y < 28:
sp = x * constantsAndConfigs.ticksToPixelRatio
blockId, blockStartOffset = self.blocKByPosition(sp) #get the block before rounding. Otherwise rounding might result in a position higher than the blocks duration
if constantsAndConfigs.snapToGrid:
sp = round(sp / constantsAndConfigs.gridRhythm) * constantsAndConfigs.gridRhythm
positionInBlockBackendTicks = sp - blockStartOffset
api.addGraphItem(blockId, positionInBlockBackendTicks, self.getYAsBackendValue(y))
class CCGraphTransparentBlock(QtWidgets.QGraphicsRectItem):
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.setFlags(QtWidgets.QGraphicsItem.ItemDoesntPropagateOpacityToChildren) #no mouseReleaseEvent without selection or movable.
self.parentCCPath = parent
self.parent = parent #for compatibility with block movement to the appending positon.
self.parentGuiTrack = parent.parentGuiTrack #redundant, but specifically for block movement. see ScoreScene
self.color = stringToColor(staticExportItem["name"])
self.setBrush(self.color)
self.trans = QtGui.QColor("transparent")
self.setPen(self.trans)
self.setBrush(self.trans)
self.setOpacity(0.9) #only visible during drag and move
self.staticExportItem = staticExportItem
self.blockEndMarker = CCGraphBlockEndMarker(self, staticExportItem)
self.blockEndMarker.setParentItem(self)
self.blockEndMarker.setPos(staticExportItem["duration"] / constantsAndConfigs.ticksToPixelRatio, 0)
self.posBeforeMove = None
self.cursorPosOnMoveStart = None
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.blockEndMarker.setX(self.blockEndMarker.pos().x() * factor)
def mousePressEventCustom(self, event):
"""Custom gets called by the scene mouse press event directly only when the right keys
are hold down.
e.g. shift + middle mouse button for moving this block"""
self.posBeforeMove = self.pos()
self.cursorPosOnMoveStart = QtGui.QCursor.pos()
self.setBrush(self.color)
super().mousePressEvent(event)
def mouseMoveEventCustom(self, event):
"""Custom gets called by the scene mouse press event directly only when the right keys
are hold down
e.g. shift + middle mouse button for moving this block"""
# 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.parentCCPath.parentScore.parentView.mapFromGlobal(posGlobal) #a widget
#posScene = self.parentCCPath.parentScore.parentView.mapToScene(posView)
#print (posGlobal, posView, posScene, event.scenePos())
if self.cursorPosOnMoveStart:
self.setPos(event.scenePos())
"""
#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())
self.setPos(0, new.y())
else:
#self.setPos(new.x(), self.posBeforeMove.y()) #only move in one track
self.setPos(new.x(), new.y())
#event.ignore() #this blocks the qt movable object since we already move the object on our own.
"""
super().mouseMoveEvent(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.splitCCBlock(self.staticExportItem["id"], int(posRelativeToBlockStart))
def contextMenuEvent(self, event):
listOfLabelsAndFunctions = [
(translate("ccblock", "split here"), lambda: self.splitHere(event)),
(translate("ccblock", "duplicate"), lambda: api.duplicateCCBlock(self.staticExportItem["id"])),
(translate("ccblock", "create content link"), lambda: api.duplicateContentLinkCCBlock(self.staticExportItem["id"])),
(translate("ccblock", "unlink"), lambda: api.unlinkCCBlock(self.staticExportItem["id"])),
("separator", None),
(translate("ccblock", "move to start"), lambda: api.moveCCBlockToStartOfTrack(self.staticExportItem["id"])),
(translate("ccblock", "move to end"), lambda: api.moveCCBlockToEndOfTrack(self.staticExportItem["id"])),
("separator", None),
(translate("ccblock", "join with next block"), lambda: api.mergeWithNextGraphBlock(self.staticExportItem["id"])),
(translate("ccblock", "delete block"), lambda: api.deleteCCBlock(self.staticExportItem["id"])),
("separator", None),
(translate("ccblock", "append block at the end"), lambda ccNum = self.parentCCPath.getCCNumber(): api.appendGraphBlock(self.parentCCPath.parentDataTrackId, ccNum)),
]
callContextMenu(listOfLabelsAndFunctions)
class CCGraphBlockEndMarker(QtWidgets.QGraphicsLineItem):
def __init__(self, parentTransparentBlock, staticExportItem):
super(CCGraphBlockEndMarker, self).__init__(0,-28,0,28) #x, y, w, h
self.parentTransparentBlock = parentTransparentBlock
self.parentCCPath = self.parentTransparentBlock.parent
self.staticExportItem = staticExportItem
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable|QtWidgets.QGraphicsItem.ItemSendsGeometryChanges|QtWidgets.QGraphicsItem.ItemIsFocusable)
self.setAcceptHoverEvents(True)
self.setCursor(QtCore.Qt.SizeHorCursor)
self.lastPos = self.pos()
#self.setBrush(QtGui.QColor("red"))
pen = QtGui.QPen() # creates a default pen
if not self.staticExportItem["exportsAllItems"]:
pen.setStyle(QtCore.Qt.DashDotLine)
pen.setWidth(2)
self.setPen(pen)
self.setZValue(10)
self.inactivePen = pen
self.inactivePen.setColor(QtGui.QColor("black"))
self.activePen = QtGui.QPen(pen)
self.activePen.setColor(QtGui.QColor("cyan"))
def allItemsRightOfMe(self):
for item in self.parentCCPath.items:
if item.x() > self.x():
yield item
def shape(self):
"""Qt Function
Return a more accurate shape for this item so that
mouse hovering is more accurate"""
path = QtGui.QPainterPath()
path.addRect(QtCore.QRectF(-3, -1/2 * constantsAndConfigs.trackHeight, 9, constantsAndConfigs.trackHeight)) #this is directly related to inits parameter x, y, w, h
return path
def mouseReleaseEvent(self, event):
"""After moving a point around
send an update to the backend"""
super(CCGraphBlockEndMarker, self).mouseReleaseEvent(event)
if event.button() == 1: #QtCore.Qt.LeftButton
x = event.scenePos().x()
#x = self.x()
x = x * constantsAndConfigs.ticksToPixelRatio
endingRelativeToBlockStart = x - self.staticExportItem["position"]
if constantsAndConfigs.snapToGrid:
endingRelativeToBlockStart = round(endingRelativeToBlockStart / constantsAndConfigs.gridRhythm) * constantsAndConfigs.gridRhythm
assert endingRelativeToBlockStart > 0
api.changeGraphBlockDuration(self.staticExportItem["id"], endingRelativeToBlockStart)
def mousePressEvent(self, event):
super(CCGraphBlockEndMarker, self).mousePressEvent(event)
if event.button() == 1: #QtCore.Qt.LeftButton
for i in self.allItemsRightOfMe():
i.hide()
def mouseMoveEvent(self, event):
"""Only active when the item is also selected and left
mouse button is down. Not any mouse event, no hover."""
super(CCGraphBlockEndMarker, self).mouseMoveEvent(event)
self.setY(self.lastPos.y())
def hoverEnterEvent(self, event):
#self.setZValue(20)
#self.parentTransparentBlock.setZValue(19)
self.parentCCPath.hoveringOverItem = self
self.setPen(self.activePen)
self.parentTransparentBlock.setBrush(self.parentTransparentBlock.color)
def hoverLeaveEvent(self, event):
#self.setZValue(10)
#self.parentTransparentBlock.setZValue(9)
self.parentCCPath.hoveringOverItem = None #When moving the blockEndMarker this is never called because it will be deleted before hoverOut. We reset the variable when recreating though.
self.setPen(self.inactivePen)
self.parentTransparentBlock.setBrush(self.parentTransparentBlock.trans)
class CCInterpolatedPoint(QtWidgets.QGraphicsEllipseItem):
"""This is practically read-only"""
def __init__(self):
super(CCInterpolatedPoint, self).__init__(0, 0,1,1) #x,y,w,h
self.setOpacity(0.6)
self.setBrush(QtCore.Qt.black) #fill
self.setEnabled(False)
self.setZValue(-5)
self.setAcceptHoverEvents(False)
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
class CCUserPoint(QtWidgets.QGraphicsEllipseItem):
"""the position is set by the parent"""
def __init__(self, parentCCPath, staticExportItem):
super(CCUserPoint, self).__init__(-3,-3,6,6) #x,y,w,h
#color = QtGui.QColor()
#color.setHsl(127-abs(value), 255, 127) #l(h, s, l) # 8bit values. Sets a HSL color value; h is the hue, s is the saturation, l is the lightness. l goes from black(0), over color(255/2) to white (255). #0 hue is green, 128 is red
#self.setBrush(color) #fill
self.inactive = QtGui.QColor("cyan")
self.active = QtGui.QColor("black")
self.setBrush(self.inactive)
self.setFlags(QtWidgets.QGraphicsItem.ItemIsMovable|QtWidgets.QGraphicsItem.ItemSendsGeometryChanges|QtWidgets.QGraphicsItem.ItemIsFocusable)
self.setAcceptHoverEvents(True)
#self.setCursor(QtCore.Qt.SizeAllCursor)
self.parentCCPath = parentCCPath
self.staticExportItem = staticExportItem
self.interpolatedItemsRight = []
self.interpolatedItemsLeft = []
self.setZValue(9)
def shape(self):
"""Return a more accurate shape for this item so that
mouse hovering is more accurate
affects boundingRect
"""
path = QtGui.QPainterPath()
path.addEllipse(QtCore.QRectF(-5, -5, 14, 14)) #this is directly related to inits parameter. -3, -3, 6, 6.
return path
def mousePressEvent(self, event):
"""Context menu is handled by the qt function below"""
self.lastPos = self.pos()
super().mousePressEvent(event)
if event.button() == 1: #QtCore.Qt.LeftButton
self.setCursor(QtCore.Qt.BlankCursor)
for i in self.interpolatedItemsLeft + self.interpolatedItemsRight:
i.hide()
def mouseDoubleClickEvent(self, event):
self.changeCCValue()
def hoverEnterEvent(self, event):
self.setZValue(100)
self.parentCCPath.hoveringOverItem = self
self.setBrush(self.active)
self.grabKeyboard()
#self.setFocus() #do NOT set the focus. GrabKeyboard is enough. The focus will stay even after hoverLeaveEvent so that Delete will delete the last hovered item. not good!
def hoverLeaveEvent(self, event):
"""reverse hoverEnterEvent"""
self.setZValue(9)
self.setBrush(self.inactive)
self.parentCCPath.hoveringOverItem = None
self.ungrabKeyboard()
def keyPressEvent(self, event):
"""Handle the delete item key.
Needs grabKeyboard, but NOT setFocus"""
key = event.key()
if key == 16777223:
self.delete()
else:
return super().keyPressEvent(event)
def mouseReleaseEvent(self, event):
"""After moving a point around
send an update to the backend"""
super().mouseReleaseEvent(event)
self.setCursor(QtCore.Qt.SizeAllCursor)
if event.button() == 1: #QtCore.Qt.LeftButton
api.changeGraphItem(self.staticExportItem["id"], self.getXDifferenceAsBackendValue(), self.getYAsBackendValue()) #send update to the backend, don't wait for callback.
def mouseMoveEvent(self, event):
"""Only active when the item is also selected and left
mouse button is down. Not any mouse event, no hover."""
modifiers = QtWidgets.QApplication.keyboardModifiers()
super(CCUserPoint, self).mouseMoveEvent(event) #same as setPos, but this triggers the safeguards withinBlock below, while setPos does not.
if modifiers == QtCore.Qt.ShiftModifier:
self.setY(self.lastPos.y())
elif modifiers == QtCore.Qt.AltModifier:
self.setX(self.lastPos.x())
absoluteXPosition = self.x() * constantsAndConfigs.ticksToPixelRatio
withinBlock = self.staticExportItem["minPossibleAbsolutePosition"] <= absoluteXPosition <= self.staticExportItem["maxPossibleAbsolutePosition"]
if not withinBlock:
if absoluteXPosition < self.staticExportItem["minPossibleAbsolutePosition"]:
self.setX(self.staticExportItem["minPossibleAbsolutePosition"] / constantsAndConfigs.ticksToPixelRatio)
else:
self.setX(self.staticExportItem["maxPossibleAbsolutePosition"] / constantsAndConfigs.ticksToPixelRatio)
#Make mouse cursor movement not weird.
a = self.scene().parentView.mapFromScene(self.pos())
b = self.scene().parentView.viewport().mapToGlobal(a)
QtGui.QCursor.setPos(b.x(), QtGui.QCursor.pos().y())
withinYRange = -28 <= self.y() <= 28
if not withinYRange:
if self.y() < -28:
self.setY(-27.9)
else:
self.setY(28)
#Make mouse cursor movement not weird.
a = self.scene().parentView.mapFromScene(self.pos())
b = self.scene().parentView.viewport().mapToGlobal(a)
QtGui.QCursor.setPos(QtGui.QCursor.pos().x(), b.y())
def delete(self):
"""Instruct the backend to delete this item. Will trigger a
callback to redraw the graphTrack"""
api.removeGraphItem(self.staticExportItem["id"])
def contextMenuEvent(self, event):
menu = QtWidgets.QMenu()
listOfLabelsAndFunctions = [
("interpolation Type", self.changeInterpolationType),
("CC Value", self.changeCCValue),
("delete", self.delete), #deletes the last hover item which is of course the current one.
]
for text, function in listOfLabelsAndFunctions:
if text == "separator":
self.addSeparator()
else:
a = QtWidgets.QAction(text, menu)
menu.addAction(a)
a.triggered.connect(function)
pos = QtGui.QCursor.pos()
pos.setY(pos.y() + 5)
self.ungrabMouse() #if we don't ungrab and the user clicks the context menu "away" by clicking in an empty area this will still get registered as mouseclick belonging to the current item and change the value to some insane amount, breaking the point.
menu.exec_(pos)
def changeInterpolationType(self):
graphTypeString = QtWidgets.QInputDialog.getItem(self.scene().parentView, "Interpolation Type", "choose Interpolation Type", api.getListOfGraphInterpolationTypesAsStrings(), 0, False)
if graphTypeString[1]: #[1] bool if canceled
api.changeGraphItemInterpolation(self.staticExportItem["id"], graphTypeString[0])
def changeCCValue(self):
ccValue = QtWidgets.QInputDialog.getInt(self.scene().parentView, "CC Value", "specify midi control change value", 64, 0, 127) #value, min, max.
if ccValue[1]: #[1] bool if canceled
api.changeGraphItem(self.staticExportItem["id"], self.getXDifferenceAsBackendValue(), ccValue[0]) #send update to the backend, don't wait for callback.
def getYAsBackendValue(self):
newCCValue = int(self.y() / 0.4375 - 64) #value goes from -0 to -127 and are compressed to fit between two lines in the staff.
newCCValue = abs(newCCValue)
assert 0 <= newCCValue < 128
return newCCValue
def getXDifferenceAsBackendValue(self):
"""The problem is that we need the position relativ to its
parent block, which we don't use in that way in the GUI.
So we calculate the difference to the old position"""
oldPositionAbsolute = int(self.staticExportItem["position"])
newPositionAbsolute = self.pos().x() * constantsAndConfigs.ticksToPixelRatio
if constantsAndConfigs.snapToGrid:
newPositionAbsolute = round(newPositionAbsolute / constantsAndConfigs.gridRhythm) * constantsAndConfigs.gridRhythm
diff = int(-1 * (oldPositionAbsolute - newPositionAbsolute))
return diff