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
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
|
|
|