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.
1469 lines
72 KiB
1469 lines
72 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2022, 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/>.
|
|
"""
|
|
|
|
import logging; logger = logging.getLogger(__name__); logger.info("import")
|
|
|
|
#Standard Library
|
|
from fractions import Fraction
|
|
from time import time
|
|
|
|
#Third Party
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
|
|
#Template
|
|
import template.qtgui.helper as helper
|
|
|
|
#Our modules
|
|
import engine.api as api #Session is already loaded and created, no duplication.
|
|
|
|
|
|
|
|
SIZE_UNIT = 30 #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,
|
|
"barline":5,
|
|
"group" : 6,
|
|
"switch":7,
|
|
"barlineGroupHighlight":9,
|
|
"playhead":90,
|
|
}
|
|
|
|
class SongEditor(QtWidgets.QGraphicsScene):
|
|
def __init__(self, parentView):
|
|
super().__init__()
|
|
self.parentView = parentView
|
|
self.statusMessage = self.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
|
|
|
|
#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)
|
|
|
|
self._exportDictScore = None #cached
|
|
|
|
#Subitems
|
|
self.playhead = Playhead(parentScene = self)
|
|
self.addItem(self.playhead)
|
|
self.playhead.setY(SIZE_TOP_OFFSET)
|
|
|
|
self._groupRectangles = []
|
|
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()
|
|
|
|
self.currentHoverStep = None #either a Step() object or None. Only set when hovering active steps.
|
|
|
|
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)
|
|
api.callbacks.patternLengthMultiplicatorChanged.append(self.callback_patternLengthMultiplicatorChanged)
|
|
|
|
#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)
|
|
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. Also groups and visibility."""
|
|
toDelete = set(self.tracks.keys())
|
|
self.trackOrder = []
|
|
groupsSeen = set() #check if we already know this group
|
|
|
|
for grect in self._groupRectangles:
|
|
self.removeItem(grect) #group rectangles are direct children of the scene. delete them here.
|
|
self._groupRectangles = []
|
|
|
|
groupOffset = 0 #pixels. It is a positive/absolute value
|
|
hiddenOffsetCounter = 0 #counts hidden tracks in pixels. It is a positive/absolute value
|
|
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"])
|
|
|
|
if exportDict["group"]:
|
|
if not exportDict["group"] in groupsSeen: #first encounter
|
|
groupsSeen.add(exportDict["group"])
|
|
groupRect = QtWidgets.QGraphicsRectItem(0,0, exportDict["numberOfMeasures"]*SIZE_UNIT, SIZE_UNIT)
|
|
groupRect.trackGroup = exportDict["group"] #add a marker for double clicks, so that we don't have to create a whole new class.
|
|
role = QtGui.QPalette.Window
|
|
c = self.parentView.parentMainWindow.fPalBlue.color(role)
|
|
groupRect.setBrush(c)
|
|
self.addItem(groupRect)
|
|
groupRect.setY(index * SIZE_UNIT + SIZE_TOP_OFFSET + groupOffset - hiddenOffsetCounter)
|
|
groupRect.setZValue(_zValuesRelativeToScene["group"])
|
|
self._groupRectangles.append(groupRect)
|
|
|
|
groupOffset = len(groupsSeen) * SIZE_UNIT
|
|
|
|
if exportDict["visible"]:
|
|
self.tracks[exportDict["id"]].show()
|
|
else:
|
|
self.tracks[exportDict["id"]].hide()
|
|
hiddenOffsetCounter += SIZE_UNIT
|
|
|
|
self.trackOrder.append(self.tracks[exportDict["id"]])
|
|
self.tracks[exportDict["id"]].setY(index * SIZE_UNIT + SIZE_TOP_OFFSET + groupOffset - hiddenOffsetCounter)
|
|
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]
|
|
for position, switch in trackStructure.switches.items():
|
|
self.removeItem(switch) #switches are direct children of the scene. delete them here.
|
|
#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 + groupOffset - hiddenOffsetCounter
|
|
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):
|
|
self._exportDictScore = 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()
|
|
|
|
#Adjust Group Header Rectangles
|
|
for groupRect in self._groupRectangles:
|
|
r = groupRect.rect()
|
|
r.setWidth((requestAmountOfMeasures-1) * SIZE_UNIT)
|
|
groupRect.setRect(r)
|
|
|
|
|
|
#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():
|
|
factor = track.exportDict["patternLengthMultiplicator"]
|
|
track.updateSwitchVisibility(requestAmountOfMeasures=(requestAmountOfMeasures-1) // factor)
|
|
track.updateStaffLines(requestAmountOfMeasures-1)
|
|
|
|
def callback_scoreChanged(self, exportDictScore):
|
|
self._exportDictScore = 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)
|
|
barline.setZValue(_zValuesRelativeToScene["barlineGroupHighlight"])
|
|
else:
|
|
barline.setPen(self.normalPen)
|
|
barline.setZValue(_zValuesRelativeToScene["barline"])
|
|
|
|
|
|
def callback_patternLengthMultiplicatorChanged(self, exportDict):
|
|
"""This is only for a single track. We relay it"""
|
|
track = self.tracks[exportDict["id"]]
|
|
track.updatePatternLengthMultiplicator(exportDict)
|
|
|
|
def mouseDoubleClickEvent(self, event):
|
|
item = self.itemAt(event.scenePos().x(), event.scenePos().y(), self.parentView.transform())
|
|
event.ignore() #send to child widget
|
|
if item:
|
|
try:
|
|
item.trackGroup
|
|
api.setGroupVisible(item.trackGroup)
|
|
except:
|
|
pass
|
|
|
|
|
|
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.statusMessage = self.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
|
|
self.setAcceptHoverEvents(True) #for the preview highlight switch
|
|
|
|
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) #incompatible with zValues. We need this relative to the scene
|
|
self.parentScene.addItem(self._markerLine)
|
|
#self._markerLine.setY(self.pos().y()) #won't work yet. We are not in the scene ourselves. We set it in mousePressEvent, before show()
|
|
self._markerLine.setZValue(_zValuesRelativeToScene["switch"]+1) #It is not possible to have this in front of barlines AND the switches. Barlines need to be below switches for multiplied-patterns. But we want to "erase" the switches
|
|
self._markerLine.hide()
|
|
|
|
#Semitransparent hover-switch to show which one would be activated/deactivated
|
|
#Color and position is set in the callbacks and mouse handling
|
|
#It is below the actual switch so it will not show when there is already a switch, which is ok
|
|
self._highlightSwitch = QtWidgets.QGraphicsRectItem(0,0,SIZE_UNIT, SIZE_UNIT)
|
|
self._highlightSwitch.setParentItem(self)
|
|
self._highlightSwitch.setOpacity(0.2)
|
|
self._highlightSwitch.hide()
|
|
|
|
def _setColors(self, exportDict):
|
|
"""Called from various callbacks like updateSwitches and updateMetadata"""
|
|
self.exportDict = exportDict
|
|
c = QtGui.QColor(exportDict["color"])
|
|
self.currentColor = c
|
|
|
|
self._highlightSwitch.setBrush(c) #this is with low opacity.
|
|
|
|
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)
|
|
|
|
factor = exportDict["patternLengthMultiplicator"]
|
|
effectiveNumberOfMeasures = exportDict["numberOfMeasures"] // factor # //integer division
|
|
self.updateSwitchVisibility(effectiveNumberOfMeasures)
|
|
|
|
def updateMetaData(self, exportDict):
|
|
"""Color and Transposition status.
|
|
Does not get called on track structure change."""
|
|
self.exportDict = exportDict
|
|
self._setColors(exportDict)
|
|
|
|
for switch in self.switches.values():
|
|
switch.setBrush(self.currentColor)
|
|
switch.setScaleTransposeColor(self.labelColor)
|
|
switch.setHalftoneTransposeColor(self.labelColor)
|
|
switch.setStepDelayColor(self.labelColor)
|
|
switch.setAugmentationFactorColor(self.labelColor)
|
|
|
|
def updatePatternLengthMultiplicator(self, exportDict):
|
|
"""Comes via its own callback, also named callback_patternLengthMultiplicatorChanged.
|
|
The spinBox to set this is in TrackLabel"""
|
|
self.updateSwitches(exportDict) # contains exportDict caching.
|
|
effectiveNumberOfMeasures = exportDict["numberOfMeasures"] // exportDict["patternLengthMultiplicator"] # //integer division
|
|
#self.updateStaffLines(effectiveNumberOfMeasures) #we do not need to adjust the overall track length. That stays the same, no matter the factor.
|
|
|
|
def updateStaffLines(self, requestAmountOfMeasures):
|
|
"""The two horizontal lines that mark our track.
|
|
We do NOT need to handle patternLengthMultiplicator since the overall track length
|
|
stays the same. Just the measure divisions are different.
|
|
"""
|
|
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) #prevents zValue because switches are children of trackStructure. add to scene directly instead:
|
|
|
|
#For now we assume no stretch factor. One measure is one base measure.
|
|
#We set that in self.updateSwitchVisibility
|
|
self.scene().addItem(switch)
|
|
switch.setPos(position * SIZE_UNIT, self.y())
|
|
switch.setZValue(_zValuesRelativeToScene["switch"])
|
|
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"]
|
|
whichPatternsAreStepDelayed = self.exportDict["whichPatternsAreStepDelayed"]
|
|
whichPatternsHaveAugmentationFactor = self.exportDict["whichPatternsHaveAugmentationFactor"]
|
|
factor = self.exportDict["patternLengthMultiplicator"]
|
|
|
|
#requestAmountOfMeasures = requestAmountOfMeasures // factor #already is.
|
|
|
|
#Adjust highlight mouse hover to new stretch factor
|
|
r = self._highlightSwitch.rect()
|
|
r.setRight(SIZE_UNIT * factor)
|
|
self._highlightSwitch.setRect(r)
|
|
|
|
vis = self.exportDict["visible"]
|
|
|
|
for position, switch in self.switches.items():
|
|
|
|
#Deal with measures that stretch multiple base measures
|
|
switch.stretch(factor)
|
|
switch.setPos(position * SIZE_UNIT * factor, self.y())
|
|
|
|
#both position and requestAmountOfMeasures below are adjusted with scale factors
|
|
if not position in structure:
|
|
switch.hide() #Not delete because this may be just a temporary reduction of measures
|
|
switch.scaleTransposeOff()
|
|
elif position >= requestAmountOfMeasures: #switch is out of bounds. For factor 1 this is the same as not in the score-area
|
|
switch.hide()
|
|
switch.scaleTransposeOff()
|
|
else:
|
|
switch.show()
|
|
|
|
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()
|
|
|
|
if position in whichPatternsAreStepDelayed:
|
|
switch.setStepDelay(whichPatternsAreStepDelayed[position])
|
|
else:
|
|
switch.stepDelayOff()
|
|
|
|
if position in whichPatternsHaveAugmentationFactor:
|
|
switch.setAugmentationFactor(whichPatternsHaveAugmentationFactor[position])
|
|
else:
|
|
switch.augmentationFactorOff()
|
|
|
|
if not vis:
|
|
switch.hide()
|
|
|
|
|
|
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
|
|
position = position * self.exportDict["patternLengthMultiplicator"] #We need the position in the global system, without the factor
|
|
measuresPerGroup = self.parentScene.measuresPerGroupCache
|
|
offset = position % measuresPerGroup
|
|
startMeasureForGroup = position - offset
|
|
endMeasureExclusive = startMeasureForGroup + measuresPerGroup
|
|
|
|
listOfLabelsAndFunctions = [
|
|
(QtCore.QCoreApplication.translate("SongStructure", "Insert empty group before this one"), lambda: api.insertSilence(howMany=measuresPerGroup, beforeMeasureNumber=startMeasureForGroup)),
|
|
(QtCore.QCoreApplication.translate("SongStructure", "Exchange group with right neigbour"), lambda: api.exchangeSwitchGroupWithGroupToTheRight(startMeasureForGroup, endMeasureExclusive)),
|
|
(QtCore.QCoreApplication.translate("SongStructure", "Delete whole group"), 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.clearSwitchGroupModifications(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 scenePos2switchPosition(self, x):
|
|
"""Map scene coordinates to counted switch engine position"""
|
|
factor = self.exportDict["patternLengthMultiplicator"]
|
|
return int(x / SIZE_UNIT / factor)
|
|
|
|
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, sendChangeToApi=True)
|
|
if event.button() == QtCore.Qt.LeftButton: #Create a switch or continue to hold down mouse button and drag to draw -> mouseMoveEvent
|
|
assert not self._mousePressOn
|
|
position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based
|
|
self._markerLine.setX(position * SIZE_UNIT * self.exportDict["patternLengthMultiplicator"])
|
|
self._markerLine.setY(self.pos().y()) #we can't do that in init because self is not in the scene by then. markerLine is a child directly to the scene, no magic reparenting.
|
|
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
|
|
|
|
#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 mouseMoveEvent(self, event):
|
|
"""Draw new switches by holding and dragging the mouse
|
|
This is only the visual aspect. Actual calcuation and insert is in mouseReleaseEvent
|
|
The initial _markerLine is setup in mousePressEvent, where the first switch is already set as well
|
|
|
|
In Patroneo this is only triggered when left mouse button is down.
|
|
We don't set the Qt flag to always react"""
|
|
position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based
|
|
factor = self.exportDict["patternLengthMultiplicator"]
|
|
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*factor)
|
|
rect.setRight(SIZE_UNIT)
|
|
else:
|
|
right = (position - self._mousePressOn[2]) * SIZE_UNIT + SIZE_UNIT
|
|
rect.setRight(right*factor)
|
|
rect.setLeft(0)
|
|
|
|
self._markerLine.setRect(rect)
|
|
self._markerLine.show()
|
|
else:
|
|
self._markerLine.hide()
|
|
|
|
def hoverEnterEvent(self, event):
|
|
self._highlightSwitch.show()
|
|
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Empty Measure: Left click to activate. Middle click to show as shadows in current pattern. Right click for measure group options.")) #Yes, this is the track. Empty measures are not objects.
|
|
#This seemed to be a good idea but horrible UX. If you move the mouse down to edit a pattern you end up choosing the last track
|
|
#self.scene().parentView.parentMainWindow.chooseCurrentTrack(self.exportDict, sendChangeToApi=True)
|
|
|
|
def hoverLeaveEvent(self, event):
|
|
self.statusMessage("")
|
|
self._highlightSwitch.hide()
|
|
|
|
def hoverMoveEvent(self, event):
|
|
"""Snap the highlight switch to grid and stretch factor"""
|
|
#x = round((event.scenePos().x() / SIZE_UNIT)-1) * SIZE_UNIT
|
|
switchPos = self.scenePos2switchPosition(event.scenePos().x())
|
|
factor = self.exportDict["patternLengthMultiplicator"]
|
|
x = switchPos * SIZE_UNIT * factor
|
|
self._highlightSwitch.setX(x)
|
|
|
|
|
|
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.
|
|
This is directly for the switches. We also defer a call to the name label"""
|
|
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.
|
|
|
|
Not every "empty square" has a switch already. Only switches that were activated at least once.
|
|
"""
|
|
def __init__(self, parentTrackStructure, position):
|
|
self.parentTrackStructure = parentTrackStructure
|
|
self.statusMessage = self.parentTrackStructure.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
|
|
self.position = position
|
|
super().__init__(0,0,SIZE_UNIT,SIZE_UNIT)
|
|
|
|
#self.rect().setWidth(SIZE_UNIT)
|
|
|
|
self.setAcceptHoverEvents(True)
|
|
|
|
self.scaleTransposeGlyph = QtWidgets.QGraphicsSimpleTextItem("")
|
|
self.scaleTransposeGlyph.setParentItem(self)
|
|
self.scaleTransposeGlyph.setScale(0.75)
|
|
self.scaleTransposeGlyph.setPos(2,0)
|
|
self.scaleTransposeGlyph.setBrush(self.parentTrackStructure.labelColor)
|
|
self.scaleTransposeGlyph.hide()
|
|
self.scaleTranspose = 0 #default engine value, safe to assume that it will never change as default.
|
|
|
|
self.halftoneTransposeGlyph = QtWidgets.QGraphicsSimpleTextItem("")
|
|
self.halftoneTransposeGlyph.setParentItem(self)
|
|
self.halftoneTransposeGlyph.setScale(0.75)
|
|
self.halftoneTransposeGlyph.setPos(2,0) #We expect that only one of the transpose variants will be used. Therefore we place them on the same coordinates, because there is not enough space for 4 mods.
|
|
self.halftoneTransposeGlyph.setBrush(self.parentTrackStructure.labelColor)
|
|
self.halftoneTransposeGlyph.hide()
|
|
self.halftoneTranspose = 0 #default engine value, safe to assume that it will never change as default.
|
|
|
|
self.stepDelayGlyph = QtWidgets.QGraphicsSimpleTextItem("")
|
|
self.stepDelayGlyph.setParentItem(self)
|
|
self.stepDelayGlyph.setScale(0.75)
|
|
self.stepDelayGlyph.setPos(1,10)
|
|
self.stepDelayGlyph.setBrush(self.parentTrackStructure.labelColor)
|
|
self.stepDelayGlyph.hide()
|
|
self.stepDelay = 0 #default engine value, safe to assume that it will never change as default.
|
|
|
|
self.augmentationFactorGlyph = QtWidgets.QGraphicsSimpleTextItem("")
|
|
self.augmentationFactorGlyph.setParentItem(self)
|
|
self.augmentationFactorGlyph.setScale(0.75)
|
|
self.augmentationFactorGlyph.setPos(1,20)
|
|
self.augmentationFactorGlyph.setBrush(self.parentTrackStructure.labelColor)
|
|
self.augmentationFactorGlyph.hide()
|
|
self.augmentationFactor = 0 #default engine value, safe to assume that it will never change as default.
|
|
|
|
|
|
def stretch(self, factor):
|
|
"""factor assumes relative to SIZE_UNIT"""
|
|
r = self.rect()
|
|
r.setRight(SIZE_UNIT * factor)
|
|
self.setRect(r)
|
|
|
|
|
|
#Scale Transpose
|
|
|
|
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" #because - is added automatically
|
|
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 increaseScaleTranspose(self):
|
|
"""By 1. Convenience function to make code in mainWindow cleaner"""
|
|
self._bufferScaleTranspose += 1
|
|
self._setScaleTransposeLabel(self._bufferScaleTranspose)
|
|
|
|
def decreaseScaleTranspose(self):
|
|
"""By 1. Convenience function to make code in mainWindow cleaner"""
|
|
self._bufferScaleTranspose -= 1
|
|
self._setScaleTransposeLabel(self._bufferScaleTranspose)
|
|
|
|
|
|
#Halftone Transpose
|
|
|
|
def setHalftoneTranspose(self, value):
|
|
self.halftoneTranspose = value
|
|
self._setHalftoneTransposeLabel(value)
|
|
|
|
def _setHalftoneTransposeLabel(self, value):
|
|
text = ("+" if value > 0 else "") + str(value) + "h" #because - is added automatically
|
|
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 increaseHalftoneTranspose(self):
|
|
"""By 1. Convenience function to make code in mainWindow cleaner"""
|
|
self._bufferHalftoneTranspose += 1
|
|
self._setHalftoneTransposeLabel(self._bufferHalftoneTranspose)
|
|
|
|
def decreaseHalftoneTranspose(self):
|
|
"""By 1. Convenience function to make code in mainWindow cleaner"""
|
|
self._bufferHalftoneTranspose -= 1
|
|
self._setHalftoneTransposeLabel(self._bufferHalftoneTranspose )
|
|
|
|
|
|
#Step Delay
|
|
|
|
def setStepDelay(self, value):
|
|
self.stepDelay = value
|
|
self._setStepDelayLabel(value)
|
|
|
|
def _setStepDelayLabel(self, value):
|
|
text = ("+" if value > 0 else "") + "d" + str(value) #because - is added automatically
|
|
self.stepDelayGlyph.setText(text)
|
|
self.stepDelayGlyph.show()
|
|
|
|
def setStepDelayColor(self, c):
|
|
self.stepDelayGlyph.setBrush(c)
|
|
|
|
def stepDelayOff(self):
|
|
self.stepDelayGlyph.setText("")
|
|
#self.stepDelayGlyph.hide()
|
|
self.stepDelay = 0
|
|
self._bufferStepDelay = 0
|
|
|
|
def increaseStepDelay(self):
|
|
"""By 1. Convenience function to make code in mainWindow cleaner"""
|
|
self._bufferStepDelay += 1
|
|
self._setStepDelayLabel(self._bufferStepDelay)
|
|
|
|
def decreaseStepDelay(self):
|
|
"""By 1. Convenience function to make code in mainWindow cleaner"""
|
|
self._bufferStepDelay -= 1
|
|
self._setStepDelayLabel(self._bufferStepDelay)
|
|
|
|
#Augmentation Factor
|
|
|
|
def setAugmentationFactor(self, value):
|
|
self.augmentationFactor = value
|
|
self._setAugmentationFactorLabel(Fraction(value))
|
|
|
|
def _setAugmentationFactorLabel(self, value):
|
|
text = "×" + str(value)
|
|
self.augmentationFactorGlyph.setText(text)
|
|
self.augmentationFactorGlyph.show()
|
|
|
|
def setAugmentationFactorColor(self, c):
|
|
self.augmentationFactorGlyph.setBrush(c)
|
|
|
|
def augmentationFactorOff(self):
|
|
self.augmentationFactorGlyph.setText("")
|
|
#self.augmentationFactorGlyph.hide()
|
|
self.augmentationFactor = 1.0
|
|
self._bufferAugmentationFactor = 1.0
|
|
|
|
def increaseAugmentationFactor(self):
|
|
"""By 1. Convenience function to make code in mainWindow cleaner"""
|
|
self._bufferAugmentationFactor *= 2
|
|
self._setAugmentationFactorLabel(Fraction(self._bufferAugmentationFactor))
|
|
|
|
def decreaseAugmentationFactor(self):
|
|
"""By 1. Convenience function to make code in mainWindow cleaner"""
|
|
self._bufferAugmentationFactor /= 2
|
|
self._setAugmentationFactorLabel(Fraction(self._bufferAugmentationFactor))
|
|
|
|
#Events
|
|
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):
|
|
"""Only active switches"""
|
|
#self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Measure: Left click to deactivate. Middle click to show as shadows in current pattern. Shift+MouseWheel for half tone transposition. Alt+MouseWheel for in-scale transposition. Right click for measure group options."))
|
|
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Measure: Left click to deactivate. Middle click to show as shadows in current pattern. Right click for measure group options. Read the Edit menu for advanced modifications while hovering."))
|
|
self._bufferScaleTranspose = self.scaleTranspose
|
|
self._bufferHalftoneTranspose = self.halftoneTranspose
|
|
self._bufferStepDelay = self.stepDelay
|
|
self._bufferAugmentationFactor = self.augmentationFactor
|
|
self.parentTrackStructure.parentScene.currentHoverStep = self
|
|
|
|
|
|
def hoverLeaveEvent(self, event):
|
|
"""only triggered when active/shown.
|
|
When leaving a modified step it will send all changes to the api.
|
|
"""
|
|
self.parentTrackStructure.parentScene.currentHoverStep = None
|
|
self.statusMessage("")
|
|
event.accept()
|
|
|
|
#The api callback resets our buffer and values.
|
|
#That is fine except if we want to register multiple changes at once. Therefore we first copy our buffers and send the copies.
|
|
bscale = -1*self._bufferScaleTranspose
|
|
bhalftone = self._bufferHalftoneTranspose
|
|
bdelay = self._bufferStepDelay
|
|
baugment = self._bufferAugmentationFactor
|
|
|
|
#Scale Transpose. Independent of Halftone Transpose
|
|
if not bscale == self.scaleTranspose:
|
|
api.setSwitchScaleTranspose(self.parentTrackStructure.exportDict["id"], self.position, bscale) #we flip the polarity here. The receiving flip is done in the callback.
|
|
#new transpose/buffer gets set via callback
|
|
if bscale == 0:
|
|
self.scaleTransposeOff()
|
|
|
|
#Halftone Transpose. Independent of Scale Transpose
|
|
if not bhalftone == self.halftoneTranspose:
|
|
api.setSwitchHalftoneTranspose(self.parentTrackStructure.exportDict["id"], self.position, bhalftone) #half tone transposition is not flipped
|
|
#new transpose/buffer gets set via callback
|
|
if bhalftone == 0:
|
|
self.halftoneTransposeOff()
|
|
|
|
#Step Delay. Also independent.
|
|
if not bdelay == self.stepDelay:
|
|
api.setSwitchStepDelay(self.parentTrackStructure.exportDict["id"], self.position, bdelay)
|
|
#new value/buffer gets set via callback
|
|
if bdelay == 0:
|
|
self.stepDelayOff()
|
|
|
|
#Augmentation Factor. Interconnected... nah, just joking. Independent of the other stuff.
|
|
if not baugment == self.augmentationFactor:
|
|
api.setSwitchAugmentationsFactor(self.parentTrackStructure.exportDict["id"], self.position, baugment)
|
|
#new value/buffer gets set via callback
|
|
if baugment == 1.0:
|
|
self.augmentationFactorOff()
|
|
|
|
def deprecated_wheelEvent(self, event):
|
|
#We now use dedicated keyboard shortcuts and not the mousewheel anymore.
|
|
#See main window menu actions
|
|
"""Does not get triggered when switch is off.
|
|
This buffers until hoverLeaveEvent and then the new value is sent in self.hoverLeaveEvent
|
|
|
|
We want to keep normal scrolling with the mousewheel, therefore both transpose functions
|
|
need an additional key. Otherwise we get scroll on measures that are off and transpose
|
|
on measures that are active, which is very confusing.
|
|
"""
|
|
|
|
if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ShiftModifier: #half tone transposition
|
|
event.accept()
|
|
if event.delta() > 0:
|
|
self._bufferHalftoneTranspose = min(+24, self._bufferHalftoneTranspose+1)
|
|
else:
|
|
self._bufferHalftoneTranspose = max(-24, self._bufferHalftoneTranspose-1)
|
|
self._setHalftoneTransposeLabel(self._bufferHalftoneTranspose)
|
|
|
|
elif QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.AltModifier: #scale transposition
|
|
event.accept()
|
|
if event.delta() > 0:
|
|
self._bufferScaleTranspose = min(+7, self._bufferScaleTranspose+1)
|
|
else:
|
|
self._bufferScaleTranspose = max(-7, self._bufferScaleTranspose-1)
|
|
self._setScaleTransposeLabel(self._bufferScaleTranspose)
|
|
|
|
#Step Delay and Augmentation Factor are not done via mousewheel. There are not enough modifier keys left over :)
|
|
#They are instead handled by menu actions directly, in cooperations with our hover callbacks.
|
|
|
|
|
|
else: #normal scroll or zoom.
|
|
event.ignore()
|
|
#super.wheelEvent(event)
|
|
|
|
class TrackLabelEditor(QtWidgets.QGraphicsScene):
|
|
"""Only the track labels: names, colors, groups"""
|
|
def __init__(self, parentView):
|
|
super().__init__()
|
|
self.parentView = parentView
|
|
self.statusMessage = self.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
|
|
self.tracks = {} #TrackID:TrackLabel
|
|
self.groups = [] #GroupLabel()
|
|
|
|
self._cachedExportDictsInOrder = []
|
|
self._exportDictScore = None #cache
|
|
|
|
#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.numberOfMeasuresChanged.append(self.callback_setnumberOfMeasures)
|
|
api.callbacks.exportCacheChanged.append(self.cacheExportDict)
|
|
api.callbacks.patternLengthMultiplicatorChanged.append(self.callback_patternLengthMultiplicatorChanged)
|
|
|
|
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_setnumberOfMeasures(self, exportDictScore):
|
|
self._exportDictScore = exportDictScore
|
|
|
|
def callback_numberOfTracksChanged(self, exportDictList):
|
|
groupsSeen = set() #check if we already know this group
|
|
toDelete = set(self.tracks.keys())
|
|
|
|
self._cachedExportDictsInOrder = exportDictList
|
|
|
|
width = self.parentView.geometry().width()
|
|
|
|
#clean group labels. Will be recreated below
|
|
for group in self.groups:
|
|
self.removeItem(group)
|
|
|
|
groupOffset = 0 #pixels. It is a positive/absolute value
|
|
hiddenOffsetCounter = 0 #counts hidden tracks in pixels. It is a positive/absolute value
|
|
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"]])
|
|
|
|
if exportDict["group"]:
|
|
if not exportDict["group"] in groupsSeen: #first encounter
|
|
groupsSeen.add(exportDict["group"])
|
|
grouplabel = GroupLabel(parentScene=self, width=width, height=SIZE_UNIT, name=exportDict["group"], visible=exportDict["visible"])
|
|
self.addItem(grouplabel)
|
|
self.groups.append(grouplabel)
|
|
grouplabel.setY(index * SIZE_UNIT + SIZE_TOP_OFFSET + groupOffset - hiddenOffsetCounter) #group offset is still "above" the current group. If this is a group itself the offset will be extended just belowt to make room for ourselves.
|
|
|
|
groupOffset = len(groupsSeen) * SIZE_UNIT
|
|
|
|
if exportDict["visible"]:
|
|
self.tracks[exportDict["id"]].show()
|
|
else:
|
|
self.tracks[exportDict["id"]].hide()
|
|
hiddenOffsetCounter += SIZE_UNIT
|
|
|
|
|
|
self.tracks[exportDict["id"]].setY(index * SIZE_UNIT + SIZE_TOP_OFFSET + groupOffset - hiddenOffsetCounter)
|
|
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, sendChangeToApi=True)
|
|
|
|
self.cachedCombinedTrackHeight = len(self.tracks) * SIZE_UNIT + groupOffset - hiddenOffsetCounter
|
|
self.setSceneRect(0,0,width-1,self.cachedCombinedTrackHeight + 3*SIZE_TOP_OFFSET)
|
|
|
|
|
|
def callback_patternLengthMultiplicatorChanged(self, exportDict):
|
|
self.tracks[exportDict["id"]].update(exportDict) #general update function that also covers our value
|
|
|
|
def tellApiToCreateNewGroupForTrack(self, trackId):
|
|
title = QtCore.QCoreApplication.translate("TrackLabelContext", "Group Name")
|
|
info = QtCore.QCoreApplication.translate("TrackLabelContext", "Create a new group by name")
|
|
result, ok = QtWidgets.QInputDialog.getText(self.parentView, title, info) #parent, titlebar, info-text
|
|
if ok:
|
|
api.setTrackGroup(trackId, str(result))
|
|
|
|
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 item:
|
|
return
|
|
|
|
if type(item) is TrackLabel:
|
|
exportDict = item.exportDict.copy()
|
|
elif type(item.parentItem()) is TrackLabel:
|
|
exportDict = item.parentItem().exportDict.copy()
|
|
else:
|
|
return None
|
|
|
|
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
|
|
|
|
#Preare both on and off variants so we have a static string translation
|
|
stepDelayOnEntry = (QtCore.QCoreApplication.translate("TrackLabelContext", "Step Delay Wrap-Around: turn on"), lambda: api.changeTrackStepDelayWrapAround(exportDict["id"], True))
|
|
stepDelayOffEntry = (QtCore.QCoreApplication.translate("TrackLabelContext", "Step Delay Wrap-Around: turn off"), lambda: api.changeTrackStepDelayWrapAround(exportDict["id"], False))
|
|
stepDelayUse = stepDelayOffEntry if exportDict["stepDelayWrapAround"] else stepDelayOnEntry
|
|
|
|
repeatDiminishedEntryOn = (QtCore.QCoreApplication.translate("TrackLabelContext", "Repeat Diminished Pattern in itself: turn on"), lambda: api.changeTrackRepeatDiminishedPatternInItself(exportDict["id"], True))
|
|
repeatDiminishedEntryOff = (QtCore.QCoreApplication.translate("TrackLabelContext", "Repeat Diminished Pattern in itself: turn off"), lambda: api.changeTrackRepeatDiminishedPatternInItself(exportDict["id"], False))
|
|
repeatDiminishedEntryUse = repeatDiminishedEntryOff if exportDict["repeatDiminishedPatternInItself"] else repeatDiminishedEntryOn
|
|
|
|
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"])),
|
|
stepDelayUse,
|
|
repeatDiminishedEntryUse,
|
|
(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)
|
|
|
|
#Add a submenu to set the midi channel of this track. Highlight the current one
|
|
midiChannelMenu = menu.addMenu(QtCore.QCoreApplication.translate("TrackLabelContext", "Send on MIDI Channel"))
|
|
for mch in range(1, 17):
|
|
mchAction = QtWidgets.QAction(str(mch), midiChannelMenu)
|
|
midiChannelMenu.addAction(mchAction)
|
|
midiChannelCommand = lambda discard, chArg=mch: api.changeTrackMidiChannel(exportDict["id"], chArg) #discard parameter given by QAction
|
|
if exportDict["midiChannel"] == mch:
|
|
mchAction.setEnabled(False)
|
|
mchAction.triggered.connect(midiChannelCommand)
|
|
|
|
#Add a submenu for track groups. Will call the api which will send us a callback to reorder the tracks.
|
|
groupMenu = menu.addMenu(QtCore.QCoreApplication.translate("TrackLabelContext", "Group"))
|
|
newGroupAction = QtWidgets.QAction(QtCore.QCoreApplication.translate("TrackLabelContext", "New Group"), groupMenu)
|
|
groupMenu.addAction(newGroupAction)
|
|
newGroupAction.triggered.connect(lambda: self.tellApiToCreateNewGroupForTrack(exportDict["id"]))
|
|
|
|
if exportDict["group"]:
|
|
removeGroupAction = QtWidgets.QAction(QtCore.QCoreApplication.translate("TrackLabelContext", "Remove from ")+exportDict['group'], groupMenu)
|
|
groupMenu.addAction(removeGroupAction)
|
|
removeGroupAction.triggered.connect(lambda: api.setTrackGroup(exportDict["id"], "")) #empty string = no group
|
|
|
|
groupMenu.addSeparator()
|
|
#Offer existing groups
|
|
for groupString in api.getGroups():
|
|
grpAction = QtWidgets.QAction(groupString, groupMenu)
|
|
groupMenu.addAction(grpAction)
|
|
midiChannelCommand = lambda discard, grpArg=groupString: api.setTrackGroup(exportDict["id"], grpArg) #discard parameter given by QAction
|
|
if exportDict["group"] == groupString:
|
|
grpAction.setEnabled(False)
|
|
grpAction.triggered.connect(midiChannelCommand)
|
|
|
|
pos = QtGui.QCursor.pos()
|
|
pos.setY(pos.y() + 5)
|
|
self.parentView.parentMainWindow.setFocus()
|
|
menu.exec_(pos)
|
|
|
|
class TrackLabel(QtWidgets.QGraphicsRectItem):
|
|
"""One track label with color, name line edit etc.
|
|
Only gets the data when update() is called.
|
|
"""
|
|
|
|
def __init__(self, parentScene, width, height):
|
|
super().__init__(0, 0, width, height)
|
|
self.parentScene = parentScene
|
|
self.exportDict = None #set in self.update
|
|
|
|
self.statusMessage = self.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
|
|
|
|
self.setPen(QtGui.QPen(QtCore.Qt.NoPen))
|
|
self.setFlag(self.ItemIgnoresTransformations) #zoom will repostion but not make the font bigger.
|
|
|
|
#Child widgets. SIZE_UNIT is used as positioning block. Multiply yourself :)
|
|
|
|
self.positioningHandle = TrackLabel.PositioningHandle(parentTrackLabel=self)
|
|
self.positioningHandle.setParentItem(self)
|
|
|
|
|
|
|
|
self.lengthMultiplicatorSpinBox = TrackLabel.lengthMultiplicatorSpinBox(parentTrackLabel=self)
|
|
self.lengthMultiplicatorSpinBox.setParentItem(self)
|
|
|
|
|
|
self.colorButton = TrackLabel.ColorPicker(parentTrackLabel=self)
|
|
self.colorButton.setParentItem(self)
|
|
|
|
self.lineEdit = TrackLabel.NameLineEdit(parentTrackLabel=self)
|
|
self.label = QtWidgets.QGraphicsProxyWidget()
|
|
self.label.setWidget(self.lineEdit)
|
|
self.label.setParentItem(self)
|
|
|
|
self.positionButtons()
|
|
|
|
def positionButtons(self, inGroup:bool=False):
|
|
"""Used for init, but also for update to show if we are in a track-group or not by
|
|
indentation."""
|
|
if inGroup:
|
|
offsetInSizeUnit = SIZE_UNIT
|
|
else:
|
|
offsetInSizeUnit = 0
|
|
|
|
self.positioningHandle.setPos(0,0)
|
|
self.lengthMultiplicatorSpinBox.setPos(SIZE_UNIT,2)
|
|
self.colorButton.setPos(3*SIZE_UNIT+offsetInSizeUnit, 3)
|
|
self.label.setPos(4*SIZE_UNIT+3+offsetInSizeUnit,0)
|
|
|
|
class lengthMultiplicatorSpinBox(QtWidgets.QGraphicsProxyWidget):
|
|
def __init__(self, parentTrackLabel):
|
|
super().__init__()
|
|
|
|
self.parentTrackLabel = parentTrackLabel
|
|
self.setAcceptHoverEvents(True)
|
|
self.spinBox = QtWidgets.QSpinBox()
|
|
self.spinBox.setSuffix("x")
|
|
#self.spinBox.setFrame(True)
|
|
self.spinBox.setMinimum(1)
|
|
self.setWidget(self.spinBox)
|
|
self.spinBox.valueChanged.connect(self.spinBoxValueChanged) #Callback for setting is in ParentTrackLabel.update
|
|
|
|
def hoverEnterEvent(self, event):
|
|
self.parentTrackLabel.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Measure length multiplicator. Enter number or spin the mouse wheel to change."))
|
|
def hoverLeaveEvent(self, event):
|
|
self.parentTrackLabel.statusMessage("")
|
|
|
|
def spinBoxValueChanged(self):
|
|
self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict, sendChangeToApi=True)
|
|
api.setTrackPatternLengthMultiplicator(self.parentTrackLabel.exportDict["id"], self.spinBox.value())
|
|
|
|
|
|
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"))
|
|
self.setAcceptHoverEvents(True)
|
|
self.setToolTip(QtCore.QCoreApplication.translate("TrackLabel", "change track color"))
|
|
|
|
def hoverEnterEvent(self, event):
|
|
self.parentTrackLabel.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Left click to change track color"))
|
|
def hoverLeaveEvent(self, event):
|
|
self.parentTrackLabel.statusMessage("")
|
|
|
|
def mousePressEvent(self, event):
|
|
if event.button() == QtCore.Qt.LeftButton:
|
|
self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict, sendChangeToApi=True)
|
|
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.setToolTip(QtCore.QCoreApplication.translate("TrackLabel", "grab and move to reorder tracks"))
|
|
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.5)
|
|
self.arrowLabel.setPos(5,2) #try to get the center
|
|
role = QtGui.QPalette.Text
|
|
self.arrowLabel.setBrush(self.parentTrackLabel.parentScene.parentView.parentMainWindow.fPalBlue.color(role))
|
|
|
|
self._cursorPosOnMoveStart = None
|
|
|
|
self.setAcceptHoverEvents(True)
|
|
def hoverEnterEvent(self, event):
|
|
self.parentTrackLabel.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Hold left mouse button and move to reorder tracks"))
|
|
def hoverLeaveEvent(self, event):
|
|
self.parentTrackLabel.statusMessage("")
|
|
|
|
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):
|
|
"""Move track up and down. Started by mousePressEvent and finalized by mouseReleaseEvent"""
|
|
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.parentTrackLabel.setZValue(self.parentTrackLabel.zValue()+1) #in front of other tracks
|
|
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.parentTrackLabel.setZValue(self.parentTrackLabel.zValue()-1) #revert mousePressEvent's +1
|
|
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 enterEvent(self, event):
|
|
self.parentTrackLabel.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Click to select track. Double click to change track name"))
|
|
|
|
def leaveEvent(self, event):
|
|
self.parentTrackLabel.statusMessage("")
|
|
|
|
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, sendChangeToApi=True)
|
|
#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.exportDict = exportDict
|
|
self.positionButtons(inGroup=bool(exportDict["group"]))
|
|
self.lineEdit.setText(exportDict["sequencerInterface"]["name"])
|
|
self.colorButton.setBrush(QtGui.QColor(exportDict["color"]))
|
|
self.lengthMultiplicatorSpinBox.spinBox.blockSignals(True)
|
|
self.lengthMultiplicatorSpinBox.spinBox.setValue(int(exportDict["patternLengthMultiplicator"]))
|
|
self.lengthMultiplicatorSpinBox.spinBox.blockSignals(False)
|
|
|
|
def mousePressEvent(self,event):
|
|
self.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.exportDict, sendChangeToApi=True)
|
|
#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 GroupLabel(QtWidgets.QGraphicsRectItem):
|
|
"""Compatible with TrackLabel but stripped down:
|
|
No name change, no color, no multiplicator. But
|
|
a name that you can drag around
|
|
|
|
Group Labels get deleted and recreated on each change.
|
|
"""
|
|
|
|
def __init__(self, parentScene, width, height, name, visible):
|
|
super().__init__(0, 0, width, height)
|
|
|
|
self.parentScene = parentScene
|
|
self.statusMessage = self.parentScene.parentView.parentMainWindow.statusBar().showMessage #a version with the correct path of this is in every class of Patroneo
|
|
self.group = name
|
|
self.visible = visible #if that changes it will change only on creation of a GroupLabel instance
|
|
self.setPen(QtGui.QPen(QtCore.Qt.NoPen))
|
|
self.setFlag(self.ItemIgnoresTransformations) #zoom will repostion but not make the font bigger.
|
|
|
|
c = self.parentScene.parentView.parentMainWindow.fPalBlue.color(QtGui.QPalette.Window)
|
|
self.setBrush(c)
|
|
|
|
#Child widgets. SIZE_UNIT is used as positioning block. Multiply yourself :)
|
|
|
|
self._duringGroupMove = False
|
|
|
|
self.positioningHandle = GroupLabel.PositioningHandle(parentGroupLabel=self)
|
|
self.positioningHandle.setParentItem(self)
|
|
self.positioningHandle.setPos(0,0)
|
|
|
|
|
|
if visible:
|
|
name = "▼ " + name
|
|
else:
|
|
name = "▶ " + name
|
|
|
|
self.qLabel = QtWidgets.QLabel(name)
|
|
self.label = QtWidgets.QGraphicsProxyWidget()
|
|
self.label.setWidget(self.qLabel)
|
|
self.label.setParentItem(self)
|
|
self.label.setPos(3*SIZE_UNIT+3,0)
|
|
|
|
self.qLabel.setMinimumSize(QtCore.QSize(0, SIZE_UNIT))
|
|
self.qLabel.setStyleSheet("background-color: rgba(0,0,0,0)") #transparent so we see the RectItem color
|
|
|
|
self.setAcceptHoverEvents(True)
|
|
|
|
def hoverEnterEvent(self, event):
|
|
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Track Group: Double Click to show or hide. You can also double click the empty group spacers above the tracks."))
|
|
def hoverLeaveEvent(self, event):
|
|
self.statusMessage("")
|
|
|
|
def mouseDoubleClickEvent(self,event):
|
|
"""Without this no PositionHandle mouseMove and mouse Release events!!!
|
|
Also no double click"""
|
|
#super().mousePressEvent(event)
|
|
if not self.positioningHandle._cursorPosOnMoveStart: #during group move
|
|
api.setGroupVisible(self.group) #calling with one parameter toggles visibility.
|
|
|
|
#def mouseDoubleClickEvent(self, event):
|
|
# event.accept()
|
|
|
|
|
|
class PositioningHandle(QtWidgets.QGraphicsEllipseItem):
|
|
def __init__(self, parentGroupLabel):
|
|
super().__init__(0,0,SIZE_UNIT-2,SIZE_UNIT-2)
|
|
self.setToolTip(QtCore.QCoreApplication.translate("GroupLabel", "grab and move to reorder groups"))
|
|
self.parentGroupLabel = parentGroupLabel
|
|
self.setPen(QtGui.QPen(QtCore.Qt.NoPen))
|
|
role = QtGui.QPalette.ToolTipBase
|
|
c = self.parentGroupLabel.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.5)
|
|
self.arrowLabel.setPos(5,2) #try to get the center
|
|
role = QtGui.QPalette.Text
|
|
self.arrowLabel.setBrush(self.parentGroupLabel.parentScene.parentView.parentMainWindow.fPalBlue.color(role))
|
|
|
|
#self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable, bool) #!!! Prevents double click to hide.
|
|
#self.setFlag(QtWidgets.QGraphicsItem.ItemIsSelectable, bool) #!!! Prevents double click to hide.
|
|
|
|
self._cursorPosOnMoveStart = None
|
|
|
|
self.setAcceptHoverEvents(True)
|
|
|
|
def hoverEnterEvent(self, event):
|
|
self.parentGroupLabel.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Hold left mouse button and move to reorder track groups"))
|
|
def hoverLeaveEvent(self, event):
|
|
self.parentGroupLabel.statusMessage("")
|
|
|
|
def yPos2trackIndex(self, y):
|
|
"""0 based"""
|
|
pos = round(y / SIZE_UNIT)
|
|
pos = min(pos, len(self.parentGroupLabel.parentScene.tracks)-1)
|
|
return pos
|
|
|
|
def mouseMoveEvent(self, event):
|
|
if self._cursorPosOnMoveStart:
|
|
self.parentGroupLabel.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.parentGroupLabel.pos()
|
|
self._cursorPosOnMoveStart = QtGui.QCursor.pos()
|
|
self.parentGroupLabel.setZValue(self.parentGroupLabel.zValue()+1) #in front of other groups
|
|
#self.parentGroupLabel.mousePressEvent(event) #This blocks mouseMOveEvent
|
|
#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.parentGroupLabel.y()) #we need to save that first, right after this we reset the position
|
|
self.parentGroupLabel.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.parentGroupLabel.setZValue(self.parentGroupLabel.zValue()-1) #revert mousePressEvent's + 1
|
|
self._posBeforeMove = None
|
|
self._cursorPosOnMoveStart = None
|
|
api.moveGroup(self.parentGroupLabel.group, newIndex)
|
|
|
|
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)
|
|
#PlayHead height is set in SongEditor.callback_numberOfTracksChanged.
|
|
#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.
|
|
|
|
PlayHead height is set in SongEditor.callback_numberOfTracksChanged.
|
|
"""
|
|
x = int(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)
|
|
|