#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2020, 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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") 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 #Set color, otherwise it will be transparent in window managers or wayland that want that. self.backColor = QtGui.QColor(55, 61, 69) self.setBackgroundBrush(self.backColor) #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 = [] #Set color, otherwise it will be transparent in window managers or wayland that want that. self.backColor = QtGui.QColor(55, 61, 69) self.backColor = QtGui.QColor(48, 53, 60) self.setBackgroundBrush(self.backColor) 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)