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.
 
 
 
 
 
 

377 lines
17 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.pitch import simpleNoteNames
language = QtCore.QLocale().languageToString(QtCore.QLocale().language())
translatedNoteNames = simpleNoteNames[language]
from template.engine.midi import programList
#User modules
from .constantsAndConfigs import constantsAndConfigs
import engine.api as api
from .items import CC as CCItem
from .items import PolyphonicAftertouch as PolyphonicAftertouchItem
class InputCursor(QtWidgets.QGraphicsItem):
"""A singleton instance that gets attached to the cursor while it is on the screen.
Adds itself to callbacks, which is no performance problem since there it is a singleton.
There is always one mode activated and one glyph visible,
except during keypresses that manually call a function to suspend the input cursor.
"""
def __init__(self, parentScene):
super().__init__()
self.parentScene = parentScene
self.setZValue(80) #relative to scene. Below playhead
self.setEnabled(False)
self.setAcceptedMouseButtons(QtCore.Qt.NoButton)
self.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity, True)
self.setFlag(QtWidgets.QGraphicsItem.ItemHasNoContents, True) #only child items. Without this we get notImplementedError: QGraphicsItem.paint() is abstract and must be overridden
pen = QtGui.QPen(QtCore.Qt.SolidLine)
pen.setCosmetic(True)
self.layerColors = {} #int:QColor
self.durationFactor = 1 #Every note stamp with fixed duration multiplies with this. Used for prevailing dots.
self.noteStamp = QtWidgets.QGraphicsRectItem(0,0,1, constantsAndConfigs.stafflineGap) #x, y, w, h
self.noteStamp.setParentItem(self)
self.noteStamp.setPen(pen)
self.ccStamp = QtWidgets.QGraphicsPolygonItem(CCItem.triangle)
self.ccStamp.setPen(pen)
self.ccStamp.setParentItem(self)
self.polyphonicAftertouchStamp = QtWidgets.QGraphicsPolygonItem(PolyphonicAftertouchItem.triangle)
self.polyphonicAftertouchStamp.setPen(pen)
self.polyphonicAftertouchStamp.setParentItem(self)
self.channelPressureStamp = QtWidgets.QGraphicsSimpleTextItem("x")
self.channelPressureStamp.setPos(0,-4)
self.channelPressureStamp.setPen(pen)
self.channelPressureStamp.setParentItem(self)
self.pitchbendStamp = QtWidgets.QGraphicsEllipseItem(0,0,constantsAndConfigs.stafflineGap,constantsAndConfigs.stafflineGap) #x, y, w, h, same as rect
self.pitchbendStamp.setPen(pen)
self.pitchbendStamp.setParentItem(self)
self.programChangeStamp = QtWidgets.QGraphicsRectItem(0,-2, constantsAndConfigs.stafflineGap, constantsAndConfigs.stafflineGap) #x, y, w, h
self.programChangeStamp.setPen(pen)
self.programChangeStamp.setRotation(45)
self.programChangeStamp.setParentItem(self)
self.label = QtWidgets.QGraphicsSimpleTextItem()
self.label.setParentItem(self)
self.label.setPos(10, constantsAndConfigs.stafflineGap)
self.label.setZValue(10) #relative to parent item.
self.pitchPixel = None #for scene dragging
self.pitch = None #for scene dragging
self.currentStamp = None
self.currentMode = None
self.currentDuration = None
self._duringKeyPress = False #only used internally
self.duringFreehandDrawing = False #looked up by the scene in mouseEvent
self._cachedBBT = None
api.callbacks.activeLayerChanged.append(self.activeLayerChanged)
api.callbacks.layerColorChanged.append(self.layerColorChanged)
api.callbacks.bbtStatusChanged.append(self.bbtStatusChanged)
def boundingRect(self):
if self.currentStamp:
return self.currentStamp.boundingRect()
else:
return QtCore.QRectF(0,0,0,0)
def bbtStatusChanged(self, exportDict:dict):
"""
exportDict["nominator"]
exportDict["denominator"] #in our ticks
exportDict["measureInTicks"]
"""
self._cachedBBT = exportDict
def layerColorChanged(self, layerIndex:int, color:str):
c = invertColor(QtGui.QColor(color))
self.layerColors[layerIndex] = c
if layerIndex == api.getActiveLayer():
self._setStampColors(layerIndex)
def activeLayerChanged(self, layerIndex:int):
self._setStampColors(layerIndex)
def _hideStamps(self):
self.label.hide()
self.noteStamp.hide()
self.ccStamp.hide()
self.polyphonicAftertouchStamp.hide()
self.programChangeStamp.hide()
self.pitchbendStamp.hide()
self.channelPressureStamp.hide()
#TODO: the others
def _setStampColors(self, layerIndex:int):
c = self.layerColors[layerIndex]
self.noteStamp.setBrush(c)
self.ccStamp.setBrush(c)
self.polyphonicAftertouchStamp.setBrush(c)
self.programChangeStamp.setBrush(c)
self.pitchbendStamp.setBrush(c)
self.channelPressureStamp.setBrush(c)
def setNoteStampDuration(self, value:int):
"""Automatically converts to pixel. This is not an engine object so no api calls are made"""
pixel = value / constantsAndConfigs.ticksToPixelRatio
r = self.noteStamp.rect()
r.setRight(pixel)
self.noteStamp.setRect(r)
def stretchXCoordinates(self, factor:float):
"""We only need to stretch notes. CCs etc. are single point items and have no durations."""
stretchRect(self.noteStamp, factor)
def _updateLabel(self):
if self.pitch and self.pitch > 0 and self.pitch <= 127:
if self.currentMode == "note":
#labelInfo = pitch.midi_notenames_english[self.pitch] + " (" + str(self.pitch) + ")"
labelInfo = translatedNoteNames[self.pitch] + " (" + str(self.pitch) + ")"
elif self.currentMode == "cc":
labelInfo = "CC " + str(api.session.guiSharedDataToSave["lastCCtype"]) +": " + str(self.pitch)
elif self.currentMode == "polyphonicaftertouch":
labelInfo = "PA " + str(api.session.guiSharedDataToSave["lastPolyphonicAftertouchNote"]) +": " + str(self.pitch)
elif self.currentMode == "pitchbend":
labelInfo = "Pitch Bend (64=0)" +": " + str(self.pitch)
elif self.currentMode == "program":
labelInfo = "Program Change: " + str(self.pitch) + ": " + programList[self.pitch]
else:
labelInfo = str(self.pitch)
self.label.setText(labelInfo)
def setPos(self, scenePos):
# Y Position
y = round(scenePos.y() / constantsAndConfigs.stafflineGap) * constantsAndConfigs.stafflineGap
self.pitchPixel = y
self.pitch = int(127 - y / constantsAndConfigs.stafflineGap) #for scene dragging. Midi pitch between 0 and 127
if self.currentMode is None or self.pitch < 0 or self.pitch > 127: #mode is None during selections.
self.hide() #WM cursor
self.parentScene.parentView.unsetCursor()
else:
self.parentScene.parentView.setCursor(QtCore.Qt.BlankCursor)
self.show() #We are the cursor now
# X Position in Time
if self.duringFreehandDrawing:
#x = self.noteStamp.scenePos().x() #we need the absolute scenePos. No even better to rely on our own data, not qt inteference. See next line
assert not self.duringFreehandDrawing is None
x = self.duringFreehandDrawing
r = self.noteStamp.rect()
right = max(scenePos.x() - x, 4)
r.setRight(right)
self.noteStamp.setRect(r)
#y = self.noteStamp.pos().y() #discard pitch position while drawing.
else:
x = scenePos.x()
x = round(x / constantsAndConfigs.snapToGrid ) * constantsAndConfigs.snapToGrid #snap to grid and duration
if self.duringFreehandDrawing:
super().setX(x)
else:
super().setPos(x, y)
self._updateLabel()
def setDottedNotes(self, state:bool):
if state:
self.durationFactor = 1.5
else:
self.durationFactor = 1
self.setMode(self._rememberModeForKeypress)
def putEvent(self):
"""instructs the api to create a new event"""
if self.currentMode is None:
return
assert self.currentStamp
tickposition = int(self.pos().x() * constantsAndConfigs.ticksToPixelRatio)
byte1 = 127-round(self.pos().y() / constantsAndConfigs.stafflineGap)
if byte1 < 0 or byte1 > 127 or tickposition < 0:
#Clicked outside the musical score boundaries
return
elif self.currentMode == "note" and self.currentDuration:
api.createNote(tickposition, byte1, api.getActiveMedianVelocity(), self.currentDuration-1)
elif self.currentMode == "note" and self.currentDuration == None:
self.startFreeHandDrawing()
elif self.currentMode == "polyphonicaftertouch":
api.createEvent(tickposition, 0xA0, api.session.guiSharedDataToSave["lastPolyphonicAftertouchNote"], byte1) #Yes, we reverse the bytes! See user manual.
elif self.currentMode == "cc":
api.createEvent(tickposition, 0xB0, api.session.guiSharedDataToSave["lastCCtype"], byte1) #Yes, we reverse the bytes! See user manual.
elif self.currentMode == "program":
api.createEvent(tickposition, 0xC0, byte1, 0) #Byte 2 is ignored
elif self.currentMode == "pitchbend":
api.createEvent(tickposition, 0xE0, 0, byte1) #Byte 1 is ignored in Vico
elif self.currentMode == "channelpressure":
api.createEvent(tickposition, 0xD0, byte1, 0) #Byte 2 is ignored
else:
logger.warning(tickposition, byte1)
def startFreeHandDrawing(self):
self.duringFreehandDrawing = self.pos().x()
#Do NOT set a child items position. Everything is handled via the parent InputCursor item!!
#This will lead to an exponential position because we set the parent AND the child to the same distance from 0.
#self.noteStamp.setX(self.pos().x())
r = self.noteStamp.rect()
r.setWidth(4)
self.noteStamp.setRect(r)
self.noteStamp.show()
self.currentStamp = self.noteStamp
def stopFreeHandDrawing(self):
#Treshold when to start a note.
r = self.noteStamp.rect()
if r.width() > 10:
tickposition = self.duringFreehandDrawing * constantsAndConfigs.ticksToPixelRatio
byte1 = 127-round(self.pos().y() / constantsAndConfigs.stafflineGap)
dur = r.width() * constantsAndConfigs.ticksToPixelRatio
api.createNote(tickposition, byte1, api.getActiveMedianVelocity(), dur-1)
self.duringFreehandDrawing = None
self.setMode("actionSetInsertFree")
r.setWidth(4)
self.setPos(self.parentScene.lastMouseScenePos)
def temporaryToggleForKeyPresses(self, state:bool):
assert self._rememberModeForKeypress
if state:
self.setMode(None)
else:
self.setMode(self._rememberModeForKeypress)
def setMode(self, mode:str):
"""Modes are menu actions in string form.
So we need to sync them by hand, unfortunately"""
self._hideStamps()
self.currentDuration = None
self.currentStamp = None
self.currentMode = None
if mode is None: #for selection
return
self._rememberModeForKeypress = mode
self.label.show()
def note(duration):
duration = int(duration * self.durationFactor)
self.currentMode = "note"
self.currentStamp = self.noteStamp
self.currentDuration = duration
self.setNoteStampDuration(duration)
self.noteStamp.show()
if mode == "actionSetInsertD1":
note(api.D1)
elif mode == "actionSetInsertD2":
note(api.D2)
elif mode == "actionSetInsertD4":
note(api.D4)
elif mode == "actionSetInsertD8":
note(api.D8)
elif mode == "actionSetInsertD16":
note(api.D16)
elif mode == "actionSetInsertD32":
note(api.D32)
elif mode == "actionSetInsertFree":
self.currentStamp = self.noteStamp
self.currentMode = "note"
self.currentDuration = None
r = self.noteStamp.rect()
r.setWidth(4)
self.noteStamp.setRect(r)
self.noteStamp.show()
elif mode == "actionSetInsertCC":
self.currentMode = "cc"
self.currentDuration = None
self.currentStamp = self.ccStamp
self.ccStamp.show()
elif mode == "actionSetPolyphonicAftertouch":
self.currentMode = "polyphonicaftertouch"
self.currentDuration = None
self.currentStamp = self.polyphonicAftertouchStamp
self.polyphonicAftertouchStamp.show()
elif mode == "actionSetInsertPitchBend":
self.currentMode = "pitchbend"
self.currentDuration = None
self.currentStamp = self.pitchbendStamp
self.pitchbendStamp.show()
elif mode == "actionSetInsertProgramChange":
self.currentMode = "program"
self.currentDuration = None
self.currentStamp = self.programChangeStamp
self.programChangeStamp.show()
elif mode == "actionSetInsertChannelPressure":
self.currentMode = "channelpressure"
self.currentDuration = None
self.currentStamp = self.channelPressureStamp
self.channelPressureStamp.show()
elif mode == "actionSetInserToggleDot":
pass
else:
raise ValueError("Unknown Mode", mode)
self._updateLabel()