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.
 
 
 
 
 
 

514 lines
23 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
This 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")
#Standard Library Modules
#Third Party Modules
from PyQt5 import QtWidgets, QtCore, QtGui
#Template Modules
from template.qtgui.helper import stretchRect, invertColor
from template.engine.midi import programList
#User modules
from .constantsAndConfigs import constantsAndConfigs
import engine.api as api
pen = QtGui.QPen(QtCore.Qt.SolidLine)
pen.setCosmetic(True)
class _EventTraits(object):
def otherinit(self, color, freeText:str):
self.isSelected = None #set by scene selection. used in our hoverEvent
self.setPen(pen)
self.cachedExportDict = None #has position, id etc.
self.noteOffExportDict = None #for typechecking
self.setAcceptHoverEvents(True)
self.setFlag(QtWidgets.QGraphicsItem.ItemIsFocusable, True)
self.duringDurationChange = False # notes emit these to differentiate between moving an item or changing the size of an item. Can be "left" or "right"
self.setBrush(color)
self.ids = set() #IDs are set by the creating function. Most have only one, but notes have two
self.freeText = QtWidgets.QGraphicsSimpleTextItem(freeText)
self.freeText.setParentItem(self)
self.freeText.setPos(0, -1.5*constantsAndConfigs.stafflineGap)
self.freeText.setEnabled(False)
def setFreeText(self, text:str):
self.freeText.setText(text)
def _freeTextLineEditSubmenu(self):
text, dialogAccepted = QtWidgets.QInputDialog.getText(self.parentLayer.parentScore.parentView.mainWindow, QtCore.QCoreApplication.translate("items", "Set Free Text"), QtCore.QCoreApplication.translate("items", "Set Free Text"), text=self.cachedExportDict["freeText"])
if dialogAccepted:
api.setFreeText(self.cachedExportDict["id"], text)
def contextMenu(self, event):
"""Not overloaded. This is our own function.
Qt is named contextMenuEvent and does not work here because we butchered the event system"""
self.cachedExportDict["id"]
menu = QtWidgets.QMenu()
listOfLabelsAndFunctions = [
(QtCore.QCoreApplication.translate("items", "Set Free Text"), self._freeTextLineEditSubmenu),
]
for text, function in listOfLabelsAndFunctions:
a = QtWidgets.QAction(text, menu)
menu.addAction(a)
a.triggered.connect(function)
pos = QtGui.QCursor.pos()
pos.setY(pos.y() + 5)
self.parentLayer.parentScore.parentView.mainWindow.setFocus()
menu.exec_(pos)
def getEngineTickPosition(self):
"""Calculate the current engine tick position for communication with the API.
This is the beginning. Note Off needs to be calculated seperately with
self.getNoteOffEngineTickPosition"""
return int(self.pos().x() * constantsAndConfigs.ticksToPixelRatio)
def highlight(self, state:bool):
"""Highlight only comes from other view, like velocity. Otherwise there is a risk
that the user confuses this with "single item selection"""
if self.isSelected:
return
if state:
self.setBrush(self.parentLayer.selectionColor)
else:
self.setBrush(self.parentLayer.color)
def hoverEnterEvent(self, event):
if self.isSelected:
self.scene().inputCursor.temporaryToggleForKeyPresses(True)
self.setFocus() #keyboard focus
#elif not self.parentLayer.parentScore.selectedItems:
# self.parentLayer.parentScore.parentView.mainWindow.highlight(self.parentLayerIndex, self.cachedExportDict["id"], True) #make a roundtrip over the mainwindow. In the end our own highlight will be called, but also the velocityView and future items are easily possible
return super().hoverEnterEvent(event) #return has nothing to do with the functionality and is not needed in this case, but it is a good habit when a return value is actually needed
def hoverLeaveEvent(self, event):
#self.unsetCursor()
self.clearFocus()
if self.isSelected:
self.scene().inputCursor.temporaryToggleForKeyPresses(False)
#elif not self.parentLayer.parentScore.selectedItems:
# self.parentLayer.parentScore.parentView.mainWindow.highlight(self.parentLayerIndex, self.cachedExportDict["id"], False)
return super().hoverLeaveEvent(event) #return has nothing to do with the functionality and is not needed in this case, but it is a good habit when a return value is actually needed
def dont_hoverMoveEvent(self, event):
if self.isSelected:
if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.AltModifier:
self.setCursor(QtCore.Qt.SizeHorCursor)
else:
self.setCursor(QtCore.Qt.SizeVerCursor)
def stretchXCoordinates(self, factor:float):
pass
def dont_keyPressEvent(self, event):
"""Immediately change the cursor on keypress, without waiting for mouse movement"""
if self.isSelected and event.key() == QtCore.Qt.Key_Alt:
#Note to self: if this ever seems to fail remember that xbanish hides the cursor on keypress! This function works!
self.setCursor(QtCore.Qt.SizeHorCursor)
return super().keyPressEvent(event)
def dont_keyReleaseEvent(self, event):
if self.isSelected:
self.setCursor(QtCore.Qt.SizeVerCursor)
else:
self.unsetCursor()
return super().keyPressEvent(event)
def mousePressEvent(self, event):
if self.isSelected and event.button() == QtCore.Qt.LeftButton:
event.wasUsed = self
event.wasUsed.itemClickedX = event.buttonDownPos(QtCore.Qt.LeftButton).x()
#Don't! return super().mousePressEvent(event) #where does this lead? child items? If we send this this will prevent stacked items from getting selected.
else: #never executed.
return super().mousePressEvent(event)
def setPianoRollPitch(self, pitch):
"""This is a true engine value where 0 is lowest and 127 highest. Not the inverted
GraphicsScene coordinates"""
raise NotImplementedError("Decide if this means byte1 (notes) or byte2 (CCs)")
def callbackByteOne(self, value):
raise NotImplementedError("Decide if this means pianoRollPitch change or not")
def callbackByteTwo(self, value):
raise NotImplementedError("Decide if this means pianoRollPitch change or not")
EDGE_AREA_WIDTH = api.D32*1.5 / constantsAndConfigs.ticksToPixelRatio
class Note(_EventTraits, QtWidgets.QGraphicsRectItem): #resolution order is important. _EventTraits needs to come first, otherwise class _EventTraits will not find super()
def __init__(self, parentLayer, parentLayerIndex:int, pitch:int, velocity:int, color, freeText:str):
"""Position in the layer/scene is not calculated in the item itself but outside"""
super().__init__(0,0, 1, constantsAndConfigs.stafflineGap) #x, y, w, h
self.setParentItem(parentLayer)
self.parentLayer = parentLayer
self.parentLayerIndex = parentLayerIndex
self.byte1 = pitch
self.byte2 = velocity
self.leftIndicator = QtWidgets.QGraphicsRectItem(0, 0, EDGE_AREA_WIDTH, constantsAndConfigs.stafflineGap)
self.leftIndicator.setPos(0,0)
self.leftIndicator.setEnabled(False)
#self.leftIndicator.setPen(QtGui.QPen(QtCore.Qt.NoPen))
self.leftIndicator.setPen(pen)
self.leftIndicator.setParentItem(self)
self.leftIndicator.hide()
self.leftIndicator.setAcceptedMouseButtons(QtCore.Qt.NoButton)
self.rightIndicator = QtWidgets.QGraphicsRectItem(0, 0, EDGE_AREA_WIDTH, constantsAndConfigs.stafflineGap)
#position is set in self.setDuration
self.rightIndicator.setEnabled(False)
#self.rightIndicator.setPen(QtGui.QPen(QtCore.Qt.NoPen))
self.rightIndicator.setPen(pen)
self.rightIndicator.setParentItem(self)
self.rightIndicator.hide()
self.rightIndicator.setAcceptedMouseButtons(QtCore.Qt.NoButton)
self.otherinit(color, freeText)
def setBrush(self, qcolor):
super().setBrush(qcolor)
inv = invertColor(qcolor)
self.leftIndicator.setBrush(inv)
self.rightIndicator.setBrush(inv)
def setDuration(self, value:int):
"""Automatically converts to pixel. This is not an engine object so no api calls are made"""
pixel = value / constantsAndConfigs.ticksToPixelRatio
self.setDurationInPixel(pixel)
def setDurationInPixel(self, pixel:int):
r = self.rect()
r.setRight(pixel)
self.setRect(r)
self.rightIndicator.setPos(pixel-EDGE_AREA_WIDTH ,0)
def shiftEndInPixel(self, pixel:int):
#posX can stay in place
self.setDurationInPixel(self.lastStableWidth + pixel)
def shiftStartInPixel(self, pixel:int):
"""aka move left edge but keep the right"""
self.setX(self.lastStableX + pixel) #scene coordinates
self.setDurationInPixel(self.lastStableWidth - pixel)
#leftIndicator can stay in place.
def getNoteOffEngineTickPosition(self):
"""Uses the cached engine duration, not the actual rectangle size"""
#return self.getEngineTickPosition() + (self.noteOffExportDict["position"] - self.cachedExportDict["position"])
return self.getEngineTickPosition() + (self.rect().width() *constantsAndConfigs.ticksToPixelRatio )
def stretchXCoordinates(self, factor:float):
stretchRect(self, factor)
def setPianoRollPitch(self, pitch):
"""This is a true engine value where 0 is lowest and 127 highest. Not the inverted
GraphicsScene coordinates"""
self.byte1 = pitch
def callbackByteOne(self, value):
self.byte1 = value
y = (127-value) * constantsAndConfigs.stafflineGap
self.setY(y)
def callbackByteTwo(self, value):
self.byte2 = value
def mousePressEvent(self, event):
"""By scene configuration this only triggers when selected. We check ourselves again though"""
super().mousePressEvent(event) #use _EventTrait to signal the scene that the item was used and set event.wasUsed to self
if hasattr(event, "wasUsed") and event.wasUsed: #no wasUsed: for very fast clicks (double?) this mousePressEvent will trigger without going through the scene.
assert event.wasUsed is self
x = event.pos().x() # in item coordinates
if self.isSelected and x > self.rect().right() - EDGE_AREA_WIDTH:
self.duringDurationChange = "right"
elif self.isSelected and x < EDGE_AREA_WIDTH:
self.duringDurationChange = "left"
else:
self.duringDurationChange = False #if we don't reset here a note will not be movable after a single click on the edge area
def mouseReleaseEvent(self, event):
super().mouseReleaseEvent(event)
self.duringDurationChange = False
def hoverMoveEvent(self, event):
super().hoverEnterEvent(event)
x = event.pos().x() # in item coordinates
self.leftIndicator.hide()
self.rightIndicator.hide()
if self.isSelected and x > self.rect().right() - EDGE_AREA_WIDTH:
self.rightIndicator.show()
elif self.isSelected and x < EDGE_AREA_WIDTH:
self.leftIndicator.show()
def hoverLeaveEvent(self, event):
super().hoverLeaveEvent(event)
self.leftIndicator.hide()
self.rightIndicator.hide()
def dont_mouseMoveEvent(self, event):
"""Only triggers while mouse button is down.
The event parameter is different from mousePressEvent so we don't have access to
wasUsed."""
#We don't use this to change our duration because it works on the whole selection
if self.duringDurationChange == "left":
pass
elif self.duringDurationChange == "right":
pass
"""
def wheelEvent(self, event):
event.accept() #don't scroll the view.
if event.delta() > 0:
self.changeVelocity(2)
elif event.delta() < 0:
self.changeVelocity(-2)
def changeVelocity(self, relativeValue:int):
#If there is no selection use the mousewheel to change velocity
if not self.parentLayer.parentScore.selectedItems: #this "if" is here and not in wheelEvent because keyboard shortcuts and menuActions call us as well
vel = self.byte2 + relativeValue
data = {self.cachedExportDict["id"]:vel}
api.changeVelocities(data)
"""
class CC(_EventTraits, QtWidgets.QGraphicsPolygonItem): #resolution order is important. _EventTraits needs to come first, otherwise class _EventTraits will not find super()
"""CC value is the important one, the user wants to control. It is set by placing it on the
piano roll pitch grid.
Byte1, the CC type, is set as a label next to the point"""
triangle = QtGui.QPolygonF()
#Upside Down
#triangle.append(QtCore.QPointF(0,0))
#triangle.append(QtCore.QPointF(constantsAndConfigs.stafflineGap/2, constantsAndConfigs.stafflineGap))
#triangle.append(QtCore.QPointF(constantsAndConfigs.stafflineGap, 0))
#triangle.append(QtCore.QPointF(0,0))
triangle.append(QtCore.QPointF(0,0))
triangle.append(QtCore.QPointF(0, constantsAndConfigs.stafflineGap))
triangle.append(QtCore.QPointF(constantsAndConfigs.stafflineGap, constantsAndConfigs.stafflineGap/2))
triangle.append(QtCore.QPointF(0,0))
def __init__(self, parentLayer, parentLayerIndex:int, byte1:int, byte2:int, color, freeText:str):
"""Position in the layer/scene is not calculated in the item itself but outside"""
super().__init__(CC.triangle)
self.setParentItem(parentLayer)
self.parentLayer = parentLayer
self.parentLayerIndex = parentLayerIndex
self.byte1 = byte1
self.byte2 = byte2
self.otherinit(color, freeText)
self.label = QtWidgets.QGraphicsSimpleTextItem("CC " + str(byte1))
self.label.setEnabled(False) #prevents the child item from ending up in the selection
self.label.setParentItem(self)
#self.label.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresTransformations). Prevents zoom
self.label.setScale(0.5)
self.label.setPos(1.1*constantsAndConfigs.stafflineGap, 0)
def setPianoRollPitch(self, pitch):
"""This is a true engine value where 0 is lowest and 127 highest. Not the inverted
GraphicsScene coordinates"""
self.byte2 = pitch
def callbackByteOne(self, value):
self.byte1 = value
def callbackByteTwo(self, value):
self.byte2 = value
y = (127-value) * constantsAndConfigs.stafflineGap
self.setY(y)
class PolyphonicAftertouch(_EventTraits, QtWidgets.QGraphicsPolygonItem): #resolution order is important. _EventTraits needs to come first, otherwise class _EventTraits will not find super()
"""See CC. Same system. We flip the bytes for easier intuitive editing."""
triangle = QtGui.QPolygonF()
#Upside Down
triangle.append(QtCore.QPointF(0,0))
triangle.append(QtCore.QPointF(constantsAndConfigs.stafflineGap/2, constantsAndConfigs.stafflineGap))
triangle.append(QtCore.QPointF(constantsAndConfigs.stafflineGap, 0))
triangle.append(QtCore.QPointF(0,0))
#triangle.append(QtCore.QPointF(0,0))
#triangle.append(QtCore.QPointF(0, constantsAndConfigs.stafflineGap))
#triangle.append(QtCore.QPointF(constantsAndConfigs.stafflineGap, constantsAndConfigs.stafflineGap/2))
#triangle.append(QtCore.QPointF(0,0))
def __init__(self, parentLayer, parentLayerIndex:int, byte1:int, byte2:int, color, freeText:str):
"""Position in the layer/scene is not calculated in the item itself but outside"""
super().__init__(PolyphonicAftertouch.triangle)
self.setParentItem(parentLayer)
self.parentLayer = parentLayer
self.parentLayerIndex = parentLayerIndex
self.byte1 = byte1
self.byte2 = byte2
self.otherinit(color, freeText)
self.label = QtWidgets.QGraphicsSimpleTextItem("PA " + str(byte1))
self.label.setEnabled(False) #prevents the child item from ending up in the selection
self.label.setParentItem(self)
#self.label.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresTransformations). Prevents zoom
self.label.setScale(0.5)
self.label.setPos(1.1*constantsAndConfigs.stafflineGap, 0)
def setPianoRollPitch(self, pitch):
"""This is a true engine value where 0 is lowest and 127 highest. Not the inverted
GraphicsScene coordinates"""
self.byte2 = pitch
def callbackByteOne(self, value):
self.byte1 = value
def callbackByteTwo(self, value):
self.byte2 = value
y = (127-value) * constantsAndConfigs.stafflineGap
self.setY(y)
class PitchBend(_EventTraits, QtWidgets.QGraphicsEllipseItem): #resolution order is important. _EventTraits needs to come first, otherwise class _EventTraits will not find super()
"""Byte1 is always zero in Vico."""
def __init__(self, parentLayer, parentLayerIndex:int, byte1:int, byte2:int, color, freeText:str):
"""Position in the layer/scene is not calculated in the item itself but outside"""
super().__init__(0, 0, constantsAndConfigs.stafflineGap, constantsAndConfigs.stafflineGap) #x, y, w, h, like a Rect
self.setParentItem(parentLayer)
self.parentLayer = parentLayer
self.parentLayerIndex = parentLayerIndex
self.byte1 = byte1
self.byte2 = byte2
self.otherinit(color, freeText)
#self.label = QtWidgets.QGraphicsSimpleTextItem("CC " + str(byte1))
#self.label.setEnabled(False) #prevents the child item from ending up in the selection
#self.label.setParentItem(self)
#self.label.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresTransformations). Prevents zoom
#self.label.setScale(0.5)
#self.label.setPos(1.1*constantsAndConfigs.stafflineGap, 0)
def setPianoRollPitch(self, pitch):
"""This is a true engine value where 0 is lowest and 127 highest. Not the inverted
GraphicsScene coordinates"""
self.byte2 = pitch
def callbackByteOne(self, value):
self.byte1 = value
def callbackByteTwo(self, value):
self.byte2 = value
y = (127-value) * constantsAndConfigs.stafflineGap
self.setY(y)
class ProgramChange(_EventTraits, QtWidgets.QGraphicsRectItem): #resolution order is important. _EventTraits needs to come first, otherwise class _EventTraits will not find super()
def __init__(self, parentLayer, parentLayerIndex:int, byte1:int, byte2:int, color, freeText:str):
"""Position in the layer/scene is not calculated in the item itself but outside"""
super().__init__(0,-2, constantsAndConfigs.stafflineGap, constantsAndConfigs.stafflineGap) #x, y, w, h
self.setRotation(45)
assert byte2 == 0
self.setParentItem(parentLayer)
self.parentLayer = parentLayer
self.parentLayerIndex = parentLayerIndex
self.byte1 = byte1
self.byte2 = byte2
self.otherinit(color, freeText)
self.freeText.setRotation(-45) #from otherinit
self.label = QtWidgets.QGraphicsSimpleTextItem("Program Change: " + str(self.byte1) + ": " + programList[self.byte1])
self.label.setEnabled(False) #prevents the child item from ending up in the selection
self.label.setParentItem(self)
self.label.setRotation(-45)
self.label.setScale(0.75)
self.label.setPos(constantsAndConfigs.stafflineGap, -constantsAndConfigs.stafflineGap)
def setPianoRollPitch(self, pitch):
"""This is a true engine value where 0 is lowest and 127 highest. Not the inverted
GraphicsScene coordinates"""
self.byte1 = pitch
self.label.setText("Program Change: " + str(self.byte1) + ": " + programList[self.byte1])
def callbackByteOne(self, value):
self.byte1 = value
y = (127-value) * constantsAndConfigs.stafflineGap
self.setY(y)
self.label.setText("Program Change: " + str(self.byte1) + ": " + programList[self.byte1])
def callbackByteTwo(self, value):
self.byte2 = value
class ChannelPressure(_EventTraits, QtWidgets.QGraphicsSimpleTextItem): #resolution order is important. _EventTraits needs to come first, otherwise class _EventTraits will not find super()
def __init__(self, parentLayer, parentLayerIndex:int, byte1:int, byte2:int, color, freeText:str):
"""Position in the layer/scene is not calculated in the item itself but outside"""
super().__init__("x")
assert byte2 == 0
self.setParentItem(parentLayer)
self.parentLayer = parentLayer
self.parentLayerIndex = parentLayerIndex
self.byte1 = byte1
self.byte2 = byte2
self.otherinit(color, freeText)
#Shift by -4 pixels up, to adjust for the font
self.setTransform(QtGui.QTransform.fromTranslate(0, -4), True) #dx, dy
def setPianoRollPitch(self, pitch):
"""This is a true engine value where 0 is lowest and 127 highest. Not the inverted
GraphicsScene coordinates"""
self.byte1 = pitch
def callbackByteOne(self, value):
self.byte1 = value
y = (127-value) * constantsAndConfigs.stafflineGap
self.setY(y)
def callbackByteTwo(self, value):
self.byte2 = value