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.
 
 

816 lines
39 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2019, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of Patroneo ( https://www.laborejo.org )
Laborejo 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/>.
"""
from time import time
import engine.api as api #Session is already loaded and created, no duplication.
from PyQt5 import QtCore, QtGui, QtWidgets
SIZE_UNIT = 25 #this is in manual sync with timeline.py SIZE_UNIT
SIZE_TOP_OFFSET = 0
_zValuesRelativeToScene = { #Only use for objects added directly to the scene, not children.
"trackStructure":3,
"switch":4,
"barline":5,
"playhead":90,
}
class SongEditor(QtWidgets.QGraphicsScene):
def __init__(self, parentView):
super().__init__()
self.parentView = parentView
#Subitems
self.playhead = Playhead(parentScene = self)
self.addItem(self.playhead)
self.playhead.setY(SIZE_TOP_OFFSET)
self.tracks = {} #TrackID:TrackStructures
self.barlines = [] #in order
self.cachedCombinedTrackHeight = 0 #set by callback_tracksChanged
self.trackOrder = [] #contains engine-ids, set by callback_numberOfTracksChanged
role = QtGui.QPalette.BrightText
self.brightPen = QtGui.QPen(self.parentView.parentMainWindow.fPalBlue.color(role))
self.normalPen = QtGui.QPen()
api.callbacks.numberOfTracksChanged.append(self.callback_numberOfTracksChanged)
api.callbacks.timeSignatureChanged.append(self.callback_timeSignatureChanged)
api.callbacks.numberOfMeasuresChanged.append(self.callback_setnumberOfMeasures)
api.callbacks.trackStructureChanged.append(self.callback_trackStructureChanged) #updates single tracks
api.callbacks.exportCacheChanged.append(self.cacheExportDict)
api.callbacks.scoreChanged.append(self.callback_scoreChanged) #sends information about measuresPerGroup
api.callbacks.trackMetaDataChanged.append(self.callback_trackMetaDataChanged)
#self.ticksToPixelRatio = None set by callback_timeSignatureChanged
def wheelEvent(self, event):
"""zoom, otherwise ignore event"""
if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ControlModifier:
self.parentView.parentMainWindow.zoomUpperHalf(event.delta())
event.accept()
else:
event.ignore()
super().wheelEvent(event)
def callback_trackMetaDataChanged(self, exportDict):
"""This is not for the initial track creation, only for later changes"""
self.tracks[exportDict["id"]].updateMetaData(exportDict)
def cacheExportDict(self, exportDict):
"""Does not get called on structure change because callback_trackStructureChanged
also caches the exportDict """
self.tracks[exportDict["id"]].exportDict = exportDict
def callback_trackStructureChanged(self, exportDict):
"""Happens if a switch gets flipped"""
track = self.tracks[exportDict["id"]]
track.updateSwitches(exportDict)
def callback_timeSignatureChanged(self, nr, typ):
oneMeasureInTicks = nr * typ
self.ticksToPixelRatio = oneMeasureInTicks / SIZE_UNIT
def callback_numberOfTracksChanged(self, exportDictList):
"""Used for new tracks, delete track and move track"""
toDelete = set(self.tracks.keys())
self.trackOrder = []
for index, exportDict in enumerate(exportDictList):
if exportDict["id"] in self.tracks:
toDelete.remove(exportDict["id"]) #keep this track and don't delete later.
else: #new track
self.tracks[exportDict["id"]] = TrackStructure(parentScene=self)
self.addItem(self.tracks[exportDict["id"]])
self.tracks[exportDict["id"]].setZValue(_zValuesRelativeToScene["trackStructure"])
self.trackOrder.append(self.tracks[exportDict["id"]])
self.tracks[exportDict["id"]].setY(index * SIZE_UNIT + SIZE_TOP_OFFSET)
self.tracks[exportDict["id"]].updateSwitches(exportDict)
self.tracks[exportDict["id"]].updateStaffLines(exportDict["numberOfMeasures"])
#We had these tracks in the GUI but they are gone in the export. This is track delete.
for trackId in toDelete:
trackStructure = self.tracks[trackId]
#we don't need to delete from trackOrder here because that is cleared each time we call this function
del self.tracks[trackId]
self.removeItem(trackStructure) #remove from scene
del trackStructure
assert all(track.exportDict["sequencerInterface"]["index"] == self.trackOrder.index(track) for track in self.tracks.values())
self.cachedCombinedTrackHeight = len(self.tracks) * SIZE_UNIT
self.setSceneRect(0,0,exportDict["numberOfMeasures"]*SIZE_UNIT,self.cachedCombinedTrackHeight + 3*SIZE_TOP_OFFSET) #no empty space on top without a scene rect. Also a bit of leniance.
self.playhead.setLine(0, 0, 0, self.cachedCombinedTrackHeight) #(x1, y1, x2, y2)
self.adjustBarlineHeightForNewTrackCount()
def adjustBarlineHeightForNewTrackCount(self):
"""Fetches the current context itself and modifies all existing barlines.
"""
for barline in self.barlines:
barline.setLine(0,0,0,self.cachedCombinedTrackHeight)
def callback_setnumberOfMeasures(self, exportDictScore):
requestAmountOfMeasures = exportDictScore["numberOfMeasures"]
requestAmountOfMeasures += 1 #the final closing barline
maximumAmountIncludingHidden = len(self.barlines)
if requestAmountOfMeasures == maximumAmountIncludingHidden:
for l in self.barlines: l.show()
elif requestAmountOfMeasures > maximumAmountIncludingHidden: #we need more than we have. Maybe new ones.
for l in self.barlines: l.show()
for i in range(maximumAmountIncludingHidden, requestAmountOfMeasures):
barline = QtWidgets.QGraphicsLineItem(0,0,0,1) #correct length will be set below, but we need something other than 0 here
self.addItem(barline)
barline.setAcceptedMouseButtons(QtCore.Qt.NoButton) #barlines will intercept clicks on the track otherwise. We keep the horizontal stafflines blocking to prevent accidents though.
barline.setPos(i*SIZE_UNIT, SIZE_TOP_OFFSET)
barline.setEnabled(False)
barline.setZValue(_zValuesRelativeToScene["barline"])
self.barlines.append(barline)
else: #user reduced the number of barlines. We only hide, never delete.
for l in self.barlines[requestAmountOfMeasures:]:
l.hide()
#Guaranteed visible.
for l in self.barlines[:requestAmountOfMeasures]:
l.show()
self.callback_scoreChanged(exportDictScore) #colors from the start
self.adjustBarlineHeightForNewTrackCount() #otherwise only the new ones have the correct height.
self.setSceneRect(0,0,requestAmountOfMeasures*SIZE_UNIT,self.cachedCombinedTrackHeight + 3*SIZE_TOP_OFFSET) #no empty space on top without a scene rect
for track in self.tracks.values():
track.updateSwitchVisibility(requestAmountOfMeasures=requestAmountOfMeasures-1)
track.updateStaffLines(requestAmountOfMeasures-1)
def callback_scoreChanged(self, exportDictScore):
self.measuresPerGroupCache = exportDictScore["measuresPerGroup"]
for i,barline in enumerate(self.barlines):
if i > 0 and (i+1) % exportDictScore["measuresPerGroup"] == 1:
barline.setPen(self.brightPen)
else:
barline.setPen(self.normalPen)
class TrackStructure(QtWidgets.QGraphicsRectItem):
"""From left to right. Holds two lines to show the "staffline" and a number of switches,
colored rectangles to indicate where a pattern is activated on the timeline"""
def __init__(self, parentScene):
super().__init__(0,0,1,SIZE_UNIT)
self.parentScene = parentScene
self.exportDict = None #self.update gets called immediately after creation.
self.switches = {} # position:switchInstance
self.currentColor = None #set in updateMetaData
self.labelColor = None #set in updateMetaData for redable labels on our color. for example transpose number
#The track holds the horizontal lines. The number of barlines is calculated in the parentScene for all tracks at once.
self.topLine = QtWidgets.QGraphicsLineItem(0,0,1,0) #empty line is not possible. We need at least something to let self.update work with
self.topLine.setParentItem(self)
self.topLine.setPos(0,0)
self.bottomLine = QtWidgets.QGraphicsLineItem(0,0,1,0) #empty line is not possible. We need at least something to let self.update work with
self.bottomLine.setParentItem(self)
self.bottomLine.setPos(0,SIZE_UNIT)
self.topLine.setEnabled(False)
self.bottomLine.setEnabled(False)
#Interactive Marker to select several switches in a row
self._mousePressOn = None #to remember the position of a mouse click
#self._markerLine = QtWidgets.QGraphicsLineItem(0,0,10,0) #only updated, never replaced
self._markerLine = QtWidgets.QGraphicsRectItem(0,0,10,SIZE_UNIT) #only updated, never replaced
self._markerLine.setParentItem(self)
self._markerLine.setY(0) #x is set in mousePressEvent
self._markerLine.setZValue(_zValuesRelativeToScene["playhead"])
self._markerLine.hide()
def _setColors(self, exportDict):
c = QtGui.QColor(exportDict["color"])
self.currentColor = c
if c.lightness() > 127: #between 0 (for black) and 255 (for white)
labelColor = QtGui.QColor("black")
else:
labelColor = QtGui.QColor("white")
self.labelColor = labelColor #save for new switches
def updateSwitches(self, exportDict):
self.exportDict = exportDict
self._setColors(exportDict)
#Create new switches
for position in exportDict["structure"]:
if not position in self.switches:
self.switches[position] = self._createSwitch(position)
self.updateSwitchVisibility(exportDict["numberOfMeasures"])
def updateMetaData(self, exportDict):
"""Color and Transposition status.
Does not get called on track structure change."""
self._setColors(exportDict)
for switch in self.switches.values():
switch.setBrush(self.currentColor)
switch.setScaleTransposeColor(self.labelColor)
switch.setHalftoneTransposeColor(self.labelColor)
def updateStaffLines(self, requestAmountOfMeasures):
l = self.topLine.line()
l.setLength(requestAmountOfMeasures * SIZE_UNIT)
self.topLine.setLine(l)
l = self.bottomLine.line()
l.setLength(requestAmountOfMeasures * SIZE_UNIT)
self.bottomLine.setLine(l)
#Update self, which is the track background
self.setRect(0,0,requestAmountOfMeasures * SIZE_UNIT, SIZE_UNIT)
def _createSwitch(self, position):
"""Called only by self.updateSwitches
Qt can't put the same item into the scene twice. We need to create a new one each time"""
switch = Switch(parentTrackStructure=self, position=position)
assert self.currentColor
switch.setBrush(self.currentColor)
switch.setParentItem(self)
switch.setX(position * SIZE_UNIT)
return switch
def updateSwitchVisibility(self, requestAmountOfMeasures):
"""Switch pattern-visibility on and off.
This never creates or deletes switches
We assume self.exportDict is up to date
because we get called by self.updateSwitches, which saves the exportDict."""
structure = self.exportDict["structure"]
whichPatternsAreScaleTransposed = self.exportDict["whichPatternsAreScaleTransposed"]
whichPatternsAreHalftoneTransposed = self.exportDict["whichPatternsAreHalftoneTransposed"]
for position, switch in self.switches.items():
if position < requestAmountOfMeasures and position in structure:
switch.show()
else:
switch.hide() #Not delete because this may be just a temporary reduction of measures
switch.scaleTransposeOff()
if position in whichPatternsAreScaleTransposed:
switch.setScaleTranspose(-1 * whichPatternsAreScaleTransposed[position]) #we flip the polarity from "makes sense" to row based "lower is higher" here. The opposite, sending, flip is done in switch hover leave event
else:
switch.scaleTransposeOff()
if position in whichPatternsAreHalftoneTransposed:
switch.setHalftoneTranspose(whichPatternsAreHalftoneTransposed[position]) #half tone transposition is not flipped
else:
switch.halftoneTransposeOff()
def scenePos2switchPosition(self, x):
return int(x / SIZE_UNIT)
def mousePressEvent(self, event):
#First we need to find the mouse clicks position. self.switches only holds pos that were at least activated once.
#The track is only the area where the rectangles and lines meet. it is impossible to click below or right of the tracks.
#we always get a valid position this way.
if event.button() == QtCore.Qt.MiddleButton and not self._mousePressOn:
self.parentScene.parentView.parentMainWindow.patternGrid.createShadow(self.exportDict)
else:
self.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.exportDict)
if event.button() == QtCore.Qt.LeftButton:
assert not self._mousePressOn
position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based
self._markerLine.setX(position * SIZE_UNIT )
newBool = not position in self.switches or not self.switches[position].isVisible()
if newBool:
self._markerLine.setBrush(self.currentColor)
else:
self._markerLine.setBrush(self.parentScene.parentView.parentMainWindow.fPalBlue.color(QtGui.QPalette.AlternateBase)) #we are always the active track so this is our color
self._mousePressOn = (time(), self, position, newBool) #Reset to None in mouseReleaseEvent
result = api.setSwitch(self.exportDict["id"], position, newBool) #returns True if a switch happend
assert result
#elif event.button() == QtCore.Qt.RightButton and not self._mousePressOn:
#no, this is done with contextMenuEvent directly so it also reacts to the context menu keyboard key.
def contextMenuEvent(self, event):
if self._mousePressOn: #Right click can happen while the left button is still pressed down, which we don't want.
return
menu = QtWidgets.QMenu()
position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based
measuresPerGroup = self.parentScene.measuresPerGroupCache
offset = position % measuresPerGroup
startMeasureForGroup = position - offset
endMeasureExclusive = startMeasureForGroup + measuresPerGroup
listOfLabelsAndFunctions = [
(QtCore.QCoreApplication.translate("SongStructure", "Insert empty group before this one").format(measuresPerGroup), lambda: api.insertSilence(howMany=measuresPerGroup, beforeMeasureNumber=startMeasureForGroup)),
(QtCore.QCoreApplication.translate("SongStructure", "Delete whole group").format(measuresPerGroup), lambda: api.deleteSwitches(howMany=measuresPerGroup, fromMeasureNumber=startMeasureForGroup)),
(QtCore.QCoreApplication.translate("SongStructure", "Duplicate whole group including measures"), lambda: api.duplicateSwitchGroup(startMeasureForGroup, endMeasureExclusive)),
(QtCore.QCoreApplication.translate("SongStructure", "Clear all group transpositions"), lambda: api.clearSwitchGroupTranspositions(startMeasureForGroup, endMeasureExclusive)),
]
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.parentScene.parentView.parentMainWindow.setFocus()
menu.exec_(pos)
def mouseMoveEvent(self, event):
position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based
if self._mousePressOn and position != self._mousePressOn[2]:
#self._markerLine.setLine(0,0, (position - self._mousePressOn[2])*SIZE_UNIT + SIZE_UNIT/2, 0)
rect = self._markerLine.rect()
if position < 0:
position = 0
elif position + 1 > self.exportDict["numberOfMeasures"]: #position is already a switch position
position = self.exportDict["numberOfMeasures"] - 1
if position < self._mousePressOn[2]:
left = (position - self._mousePressOn[2]) * SIZE_UNIT
rect.setLeft(left)
rect.setRight(SIZE_UNIT)
else:
right = (position - self._mousePressOn[2]) * SIZE_UNIT + SIZE_UNIT
rect.setRight(right)
rect.setLeft(0)
self._markerLine.setRect(rect)
self._markerLine.show()
else:
self._markerLine.hide()
def mouseReleaseEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self._markerLine.hide()
position = self.scenePos2switchPosition(event.scenePos().x())
if position < 0:
position = 0
elif position +1 > self.exportDict["numberOfMeasures"]: #position is already a switch position
position = self.exportDict["numberOfMeasures"] -1
startTime, startTrack, startPosition, setTo = self._mousePressOn
self._mousePressOn = None
if not startPosition == position and time() - startTime > 0.4: #optimisation to spare the engine from redundant work. Also prevent hectic drag-clicking
#setTo is a bool that tells us if all the switches in our range should go on (True) or off (False). The first switch, startPosition, is already set in mousePressEvent for a better user experience.
low, high = sorted((startPosition, position)) #both included
setOfPositions = set(range(low, high+1)) #range does not include the last one, we want it in. it MUST be a set.
api.setSwitches(self.exportDict["id"], setOfPositions, setTo)
def mark(self, boolean):
"""Mark the whole Track as active or not"""
if boolean:
role = QtGui.QPalette.AlternateBase
else:
role = QtGui.QPalette.Base
c = self.parentScene.parentView.parentMainWindow.fPalBlue.color(role)
self.setBrush(c)
class Switch(QtWidgets.QGraphicsRectItem):
"""Switches live for the duration of the track. Once created they only ever get hidden/shown,
never deleted."""
def __init__(self, parentTrackStructure, position):
self.parentTrackStructure = parentTrackStructure
self.position = position
super().__init__(0,0,SIZE_UNIT,SIZE_UNIT)
self.setAcceptHoverEvents(True)
self.setZValue(_zValuesRelativeToScene["switch"])
self.scaleTransposeGlyph = QtWidgets.QGraphicsSimpleTextItem("")
self.scaleTransposeGlyph.setParentItem(self)
self.scaleTransposeGlyph.setScale(0.80)
self.scaleTransposeGlyph.setPos(2,1)
self.scaleTransposeGlyph.setBrush(self.parentTrackStructure.labelColor)
self.scaleTransposeGlyph.hide()
self.scaleTranspose = 0
self.halftoneTransposeGlyph = QtWidgets.QGraphicsSimpleTextItem("")
self.halftoneTransposeGlyph.setParentItem(self)
self.halftoneTransposeGlyph.setScale(0.80)
self.halftoneTransposeGlyph.setPos(1,13)
self.halftoneTransposeGlyph.setBrush(self.parentTrackStructure.labelColor)
self.halftoneTransposeGlyph.hide()
self.halftoneTranspose = 0
def setScaleTranspose(self, value):
"""
Called by track callbacks and also for the temporary buffer display
while internally both the engine and us, the GUI, use steps and transposition through
"negative is higher pitch" we present it reversed for the user.
Greater number is higher pitch
It is guaranteed that only active switches can have a transposition.
Also transposition=0 is not included.
"""
self.scaleTranspose = value
self._setScaleTransposeLabel(value)
def _setScaleTransposeLabel(self, value):
text = ("+" if value > 0 else "") + str(value) + "s"
self.scaleTransposeGlyph.setText(text)
self.scaleTransposeGlyph.show()
def setScaleTransposeColor(self, c):
self.scaleTransposeGlyph.setBrush(c)
def scaleTransposeOff(self):
self.scaleTransposeGlyph.setText("")
#self.scaleTransposeGlyph.hide()
self.scaleTranspose = 0
self._bufferScaleTranspose = 0
def setHalftoneTranspose(self, value):
self.halftoneTranspose = value
self._setHalftoneTransposeLabel(value)
def _setHalftoneTransposeLabel(self, value):
text = ("+" if value > 0 else "") + str(value) + "h"
self.halftoneTransposeGlyph.setText(text)
self.halftoneTransposeGlyph.show()
def setHalftoneTransposeColor(self, c):
self.halftoneTransposeGlyph.setBrush(c)
def halftoneTransposeOff(self):
self.halftoneTransposeGlyph.setText("")
#self.halftoneTransposeGlyph.hide()
self.halftoneTranspose = 0
self._bufferhalftoneTranspose = 0
def mousePressEvent(self, event):
"""A mouse events on the track activate a switch. Then we receive the event to turn it
off again."""
event.ignore()
def hoverEnterEvent(self, event):
self._bufferScaleTranspose = self.scaleTranspose
self._bufferHalftoneTranspose = self.halftoneTranspose
def hoverLeaveEvent(self, event):
"""only triggered when active/shown"""
event.accept()
#Scale Transpose. Independent of Halftone Transpose
if not self._bufferScaleTranspose == self.scaleTranspose:
api.setSwitchScaleTranspose(self.parentTrackStructure.exportDict["id"], self.position, -1*self._bufferScaleTranspose) #we flip the polarity here. The receiving flip is done in the callback.
#new transpose/buffer gets set via callback
if self._bufferScaleTranspose == 0:
self.scaleTransposeOff()
#Halftone Transpose. Independent of Scale Transpose
if not self._bufferHalftoneTranspose == self.halftoneTranspose:
api.setSwitchHalftoneTranspose(self.parentTrackStructure.exportDict["id"], self.position, self._bufferHalftoneTranspose) #half tone transposition is not flipped
#new transpose/buffer gets set via callback
if self._bufferHalftoneTranspose == 0:
self.halftoneTransposeOff()
def wheelEvent(self, event):
"""Does not get triggered when switch is off.
This buffers until hoverLeaveEvent and then the new value is sent in self.hoverLeaveEvent"""
event.accept()
if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ShiftModifier: #half tone transposition
if event.delta() > 0:
self._bufferHalftoneTranspose = min(+24, self._bufferHalftoneTranspose+1)
else:
self._bufferHalftoneTranspose = max(-24, self._bufferHalftoneTranspose-1)
self._setHalftoneTransposeLabel(self._bufferHalftoneTranspose)
else: #scale transposition
if event.delta() > 0:
self._bufferScaleTranspose = min(+7, self._bufferScaleTranspose+1)
else:
self._bufferScaleTranspose = max(-7, self._bufferScaleTranspose-1)
self._setScaleTransposeLabel(self._bufferScaleTranspose)
class TrackLabelEditor(QtWidgets.QGraphicsScene):
"""Only the track labels"""
def __init__(self, parentView):
super().__init__()
self.parentView = parentView
self.tracks = {} #TrackID:TrackStructures
self._cachedExportDictsInOrder = []
api.callbacks.numberOfTracksChanged.append(self.callback_numberOfTracksChanged)
api.callbacks.trackMetaDataChanged.append(self.callback_trackMetaDataChanged)
api.callbacks.exportCacheChanged.append(self.cacheExportDict)
self.cachedCombinedTrackHeight = 0 #set by callback_tracksChanged
def cacheExportDict(self, exportDict):
self.tracks[exportDict["id"]].exportDict = exportDict
def callback_trackMetaDataChanged(self, exportDict):
"""This is not for the initial track creation, only for later changes"""
self.tracks[exportDict["id"]].update(exportDict)
def callback_numberOfTracksChanged(self, exportDictList):
toDelete = set(self.tracks.keys())
self._cachedExportDictsInOrder = exportDictList
width = self.parentView.geometry().width()
for index, exportDict in enumerate(exportDictList):
if exportDict["id"] in self.tracks:
toDelete.remove(exportDict["id"])
else: #new track
self.tracks[exportDict["id"]] = TrackLabel(parentScene=self, width=width, height=SIZE_UNIT)
self.addItem(self.tracks[exportDict["id"]])
self.tracks[exportDict["id"]].setY(index * SIZE_UNIT + SIZE_TOP_OFFSET)
self.tracks[exportDict["id"]].update(exportDict)
#We had this tracks in the GUI but they are gone in the export. This is track delete.
for trackId in toDelete:
trackLabel = self.tracks[trackId]
del self.tracks[trackId]
self.removeItem(trackLabel) #remove from scene
del trackLabel
if toDelete and self.parentView.parentMainWindow.currentTrackId in toDelete: #toDelete still exist if tracks were deleted above
anyExistingTrack = next(iter(self.tracks.values()))
self.parentView.parentMainWindow.chooseCurrentTrack(anyExistingTrack.exportDict)
self.cachedCombinedTrackHeight = len(self.tracks) * SIZE_UNIT
self.setSceneRect(0,0,width-1,self.cachedCombinedTrackHeight + 3*SIZE_TOP_OFFSET)
def contextMenuEvent(self, event):
"""
We can't delete this properly object from within. The engine callback will react faster
than we need to finish this function. That means qt and python will try to access
objects that are non-existent"""
menu = QtWidgets.QMenu()
item = self.itemAt(event.scenePos().x(), event.scenePos().y(), self.parentView.transform())
if not type(item) is QtWidgets.QGraphicsProxyWidget:
return None
exportDict = item.parentItem().exportDict.copy()
del item #yes, manual memory management in Python. We need to get rid of anything we want to delete later or Qt will crash because we have a python wrapper without a qt object
listOfLabelsAndFunctions = [
(exportDict["sequencerInterface"]["name"], None),
(QtCore.QCoreApplication.translate("TrackLabelContext", "Invert Measures"), lambda: api.trackInvertSwitches(exportDict["id"])),
(QtCore.QCoreApplication.translate("TrackLabelContext", "All Measures On"), lambda: api.trackOnAllSwitches(exportDict["id"])),
(QtCore.QCoreApplication.translate("TrackLabelContext", "All Measures Off"), lambda: api.trackOffAllSwitches(exportDict["id"])),
(QtCore.QCoreApplication.translate("TrackLabelContext", "Clone this Track"), lambda: api.createSiblingTrack(exportDict["id"])),
(QtCore.QCoreApplication.translate("TrackLabelContext", "Delete Track"), lambda: api.deleteTrack(exportDict["id"])),
]
for text, function in listOfLabelsAndFunctions:
if function is None:
l = QtWidgets.QLabel(text)
l.setAlignment(QtCore.Qt.AlignCenter)
a = QtWidgets.QWidgetAction(menu)
a.setDefaultWidget(l)
menu.addAction(a)
else:
a = QtWidgets.QAction(text, menu)
menu.addAction(a)
a.triggered.connect(function)
#Add a submenu for merge/cop
mergeMenu = menu.addMenu(QtCore.QCoreApplication.translate("TrackLabelContext", "Merge/Copy Measure-Structure from"))
def createCopyMergeLambda(srcId):
return lambda: api.trackMergeCopyFrom(srcId, exportDict["id"])
for sourceDict in self._cachedExportDictsInOrder:
a = QtWidgets.QAction(sourceDict["sequencerInterface"]["name"], mergeMenu)
mergeMenu.addAction(a)
mergeCommand = createCopyMergeLambda(sourceDict["id"])
if sourceDict["id"] == exportDict["id"]:
a.setEnabled(False)
a.triggered.connect(mergeCommand)
#Add a submenu for pattern merge/copy
copyMenu = menu.addMenu(QtCore.QCoreApplication.translate("TrackLabelContext", "Replace Pattern with"))
def replacePatternWithLambda(srcId):
return lambda: api.trackPatternReplaceFrom(srcId, exportDict["id"])
for sourceDict in self._cachedExportDictsInOrder:
a = QtWidgets.QAction(sourceDict["sequencerInterface"]["name"], copyMenu)
copyMenu.addAction(a)
mergeCommand = replacePatternWithLambda(sourceDict["id"])
if sourceDict["id"] == exportDict["id"]:
a.setEnabled(False)
a.triggered.connect(mergeCommand)
pos = QtGui.QCursor.pos()
pos.setY(pos.y() + 5)
self.parentView.parentMainWindow.setFocus()
menu.exec_(pos)
class TrackLabel(QtWidgets.QGraphicsRectItem):
def __init__(self, parentScene, width, height):
super().__init__(0, 0, width, height)
self.parentScene = parentScene
self.setPen(QtGui.QPen(QtCore.Qt.NoPen))
self.positioningHandle = TrackLabel.PositioningHandle(parentTrackLabel=self)
self.positioningHandle.setParentItem(self)
self.positioningHandle.setPos(0,0)
self.positioningHandle.setToolTip(QtCore.QCoreApplication.translate("TrackLabel", "grab and move to reorder tracks"))
self.colorButton = TrackLabel.ColorPicker(parentTrackLabel=self)
self.colorButton.setParentItem(self)
self.colorButton.setPos(SIZE_UNIT, 3)
self.colorButton.setToolTip(QtCore.QCoreApplication.translate("TrackLabel", "change track color"))
self.lineEdit = TrackLabel.NameLineEdit(parentTrackLabel=self)
self.label = QtWidgets.QGraphicsProxyWidget()
self.label.setWidget(self.lineEdit)
self.label.setParentItem(self)
self.label.setPos(2*SIZE_UNIT+3,0)
self.setFlag(self.ItemIgnoresTransformations)
class ColorPicker(QtWidgets.QGraphicsRectItem):
def __init__(self, parentTrackLabel):
super().__init__(0,0,SIZE_UNIT*0.75,SIZE_UNIT*0.75)
self.parentTrackLabel = parentTrackLabel
self.setBrush(QtGui.QColor("cyan"))
def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton:
self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict)
event.accept()
colorDialog = QtWidgets.QColorDialog()
color = colorDialog.getColor(self.brush().color()) #blocks
if color.isValid(): #and not abort
#self.setBrush(color) #done via callback.
api.changeTrackColor(self.parentTrackLabel.exportDict["id"], color.name())
#else:
# colorDialog.setStandardColor(self.brush().color())
else:
event.ignore()
#super().mousePressEvent(event)
class PositioningHandle(QtWidgets.QGraphicsEllipseItem):
def __init__(self, parentTrackLabel):
super().__init__(0,0,SIZE_UNIT-2,SIZE_UNIT-2)
self.parentTrackLabel = parentTrackLabel
self.setPen(QtGui.QPen(QtCore.Qt.NoPen))
role = QtGui.QPalette.ToolTipBase
c = self.parentTrackLabel.parentScene.parentView.parentMainWindow.fPalBlue.color(role)
self.setBrush(c)
self.setOpacity(0.08) #this is meant as a slight overlay/highlight of both the current track and the other tracks
self.arrowLabel = QtWidgets.QGraphicsSimpleTextItem("")
self.arrowLabel.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity)
self.arrowLabel.setParentItem(self)
self.arrowLabel.setScale(1.6)
self.arrowLabel.setPos(2,1)
role = QtGui.QPalette.Text
self.arrowLabel.setBrush(self.parentTrackLabel.parentScene.parentView.parentMainWindow.fPalBlue.color(role))
self._cursorPosOnMoveStart = None
def yPos2trackIndex(self, y):
"""0 based"""
pos = round(y / SIZE_UNIT)
pos = min(pos, len(self.parentTrackLabel.parentScene.tracks)-1)
return pos
def mouseMoveEvent(self, event):
if self._cursorPosOnMoveStart:
self.parentTrackLabel.setY(max(0, event.scenePos().y()))
#super().mouseMoveEvent(event) #with this the sync between cursor and item is off.
def mousePressEvent(self, event):
"""release gets only triggered when mousePressEvent was on the same item.
We don't need to worry about the user just releasing the mouse on this item"""
self._posBeforeMove = self.parentTrackLabel.pos()
self._cursorPosOnMoveStart = QtGui.QCursor.pos()
self._lineCursor = self.parentTrackLabel.lineEdit.cursor()
self.parentTrackLabel.mousePressEvent(event)
#super().mousePressEvent(event) #with this in mouseMoveEvent does not work. IIRC because we do not set the movableFlag
def mouseReleaseEvent(self, event):
newIndex = self.yPos2trackIndex(self.parentTrackLabel.y()) #we need to save that first, right after this we reset the position
self.parentTrackLabel.setPos(self._posBeforeMove) #In case the block was moved to a position where no track is (below the tracks) we just reset the graphics before anything happens. The user will never see this really
self._posBeforeMove = None
self._cursorPosOnMoveStart = None
api.moveTrack(self.parentTrackLabel.exportDict["id"], newIndex)
class NameLineEdit(QtWidgets.QLineEdit):
def __init__(self, parentTrackLabel):
super().__init__("")
self.parentTrackLabel = parentTrackLabel
self.setFrame(False)
self.setMaxLength(25)
self.setMinimumSize(QtCore.QSize(0, SIZE_UNIT))
self.setStyleSheet("background-color: rgba(0,0,0,0)") #transparent so we see the RectItem color
self.setReadOnly(True)
self.setFocusPolicy(QtCore.Qt.ClickFocus) #nmo tab
self.editingFinished.connect(self.sendToEngine)
self.returnPressed.connect(self.enter)
def mousePressEvent(self,event):
"""We also need to force this track as active"""
event.accept() #we need this for doubleClick
self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict)
#event.ignore() #send to parent instead
#super().mousePressEvent(event)
def mouseDoubleClickEvent(self, event):
event.accept()
self.setReadOnly(False)
def enter(self):
self.sendToEngine()
def sendToEngine(self):
self.setReadOnly(True)
new = self.text()
if not new == self.parentTrackLabel.exportDict["sequencerInterface"]["name"]:
self.blockSignals(True)
api.changeTrackName(self.parentTrackLabel.exportDict["id"], new)
self.blockSignals(False)
#def keyPressEvent(self, event):
# if event.key()) == QtCore.Qt.Key_Return:
# event.accept()
#
# else:
# event.ignore()
# super().keyPressEvent(event)
def update(self, exportDict):
self.lineEdit.setText(exportDict["sequencerInterface"]["name"])
self.exportDict = exportDict
self.colorButton.setBrush(QtGui.QColor(exportDict["color"]))
def mousePressEvent(self,event):
self.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.exportDict)
#event.ignore() #don't add that here. Will destroy mouseMove and Release events of child widgets
#def mouseReleaseEvent(self, event):
# event.
def mark(self, boolean):
if boolean:
role = QtGui.QPalette.AlternateBase
else:
role = QtGui.QPalette.Base
c = self.parentScene.parentView.parentMainWindow.fPalBlue.color(role)
self.setBrush(c)
class Playhead(QtWidgets.QGraphicsLineItem):
def __init__(self, parentScene):
super().__init__(0, 0, 0, 0) # (x1, y1, x2, y2)
self.parentScene = parentScene
p = QtGui.QPen()
p.setColor(QtGui.QColor("red"))
p.setWidth(3)
#p.setCosmetic(True)
self.setPen(p)
api.callbacks.setPlaybackTicks.append(self.setCursorPosition)
self.setZValue(_zValuesRelativeToScene["playhead"])
def setCursorPosition(self, tickindex, playbackStatus):
"""Set the playhead to the right position, but keep the viewport stable.
Shift the entire "page" if the cursor becomes invisible because its steps outside the viewport"""
x = tickindex / self.parentScene.ticksToPixelRatio
self.setX(x)
if playbackStatus: # api.duringPlayback:
scenePos = self.parentScene.parentView.mapFromScene(self.pos())
cursorViewPosX = scenePos.x() #the cursor position in View coordinates
width = self.parentScene.parentView.geometry().width()
if cursorViewPosX <= 0 or cursorViewPosX >= width: #"pageflip"
self.parentScene.parentView.horizontalScrollBar().setValue(x)