Browse Source

pattern length extended wip

master
Nils 3 years ago
parent
commit
15b985ace9
  1. 23
      engine/api.py
  2. 22
      engine/pattern.py
  3. 17
      engine/track.py
  4. 15
      qtgui/designer/mainwindow.py
  5. 12
      qtgui/designer/mainwindow.ui
  6. 5
      qtgui/mainwindow.py
  7. 61
      qtgui/pattern_grid.py
  8. 160
      qtgui/songeditor.py

23
engine/api.py

@ -37,6 +37,7 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
self.subdivisionsChanged = []
self.quarterNotesPerMinuteChanged = []
self.loopChanged = []
self.patternLengthMultiplicatorChanged = []
def _quarterNotesPerMinuteChanged(self):
"""There is one tempo for the entire song in quarter notes per mintue.
@ -174,6 +175,11 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
func(export)
callbacks._dataChanged()
def _patternLengthMultiplicatorChanged(self, track):
export = track.export()
for func in self.patternLengthMultiplicatorChanged:
func(export)
self._patternChanged(track) #includes dataChanged
#Inject our derived Callbacks into the parent module
template.engine.api.callbacks = ClientCallbacks()
@ -365,6 +371,7 @@ def createSiblingTrack(trackId):
assert type(track.pattern.scale) == tuple
newTrack = session.data.addTrack(name=track.sequencerInterface.name, scale=track.pattern.scale, color=track.color, simpleNoteNames=track.pattern.simpleNoteNames) #track name increments itself from "Track A" to "Track B" or "Track 1" -> "Track 2"
newTrack.pattern.averageVelocity = track.pattern.averageVelocity
newTrack.patternLengthMultiplicator = track.patternLengthMultiplicator
jackConnections = cbox.JackIO.get_connected_ports(track.sequencerInterface.cboxPortName())
for port in jackConnections:
cbox.JackIO.port_connect(newTrack.sequencerInterface.cboxPortName(), port)
@ -394,6 +401,22 @@ def moveTrack(trackId, newIndex):
session.data.tracks.insert(newIndex, track)
callbacks._numberOfTracksChanged()
def setTrackPatternLengthMultiplicator(trackId, newMultiplicator:int):
if newMultiplicator < 1 or not isinstance(newMultiplicator, int):
return #Invalid input
track = session.data.trackById(trackId)
track.patternLengthMultiplicator = newMultiplicator
track.pattern.buildExportCache()
track.buildTrack()
updatePlayback()
#Order is important! Otherwise the GUI doesn't know that new empty steps need to exist to fill in.
callbacks._patternLengthMultiplicatorChanged(track)
callbacks._patternChanged(track)
#Track Switches
def setSwitches(trackId, setOfPositions, newBool):

22
engine/pattern.py

@ -62,8 +62,19 @@ class Pattern(object):
self.data = data if data else list() #For content see docstring. this cannot be the default parameter because we would set the same list for all instances.
self.simpleNoteNames = simpleNoteNames if simpleNoteNames else self.parentTrack.parentData.lastUsedNotenames[:] #This is mostly for the GUI or other kinds of representation instead midi notes
assert self.simpleNoteNames
#2.0 Pitch
#Explanation: Why don't we just do 12 steps per ocatve and then leave steps blank to create a scale?
#We still want the: User set steps, not pitch idiom.
#self.stepsPerOctave = 7 # This is not supposed to go below 7! e.g. Pentatonic scales are done by leaving steps out with self.blankSteps.
#self.nrOfSteps = 8 # Needs to be >= stepsPerOctave. stepsPerOctave+1 will, by default, result in a full scale plus its octave. That can of course later be changed.
#self.blankSteps = [] # Finally, some of the steps, relative to the octave, will be left blank and mute. This should create a visible gap in the GUI. Use this for pentatonic.
self._processAfterInit()
def _prepareBeforeInit(self):
self._cachedTransposedScale = {}
@ -239,7 +250,7 @@ class Pattern(object):
self.exportCache = [] #only used by parentTrack.export()
assert self.scale and len(self.scale) == NUMBER_OF_STEPS #from constants
for pattern in (p for p in self.data if p["index"] < self.parentTrack.parentData.howManyUnits): # < and not <= because index counts from 0 but howManyUnits counts from 1
for pattern in (p for p in self.data if p["index"] < self.parentTrack.parentData.howManyUnits * self.parentTrack.patternLengthMultiplicator): # < and not <= because index counts from 0 but howManyUnits counts from 1
note = {}
note["pitch"] = pattern["pitch"]
note["index"] = pattern["index"]
@ -253,7 +264,7 @@ class Pattern(object):
self.averageVelocity = int(median(n["velocity"] for n in self.data)) if self.data else DEFAULT_VELOCITY
self._exportCacheVersion += 1 #used by build pattern for its cache hash
def buildPattern(self, scaleTransposition, halftoneTransposition, howManyUnits, whatTypeOfUnit, subdivisions):
def buildPattern(self, scaleTransposition, halftoneTransposition, howManyUnits, whatTypeOfUnit, subdivisions, patternLengthMultiplicator):
"""return a cbox pattern ready to insert into a cbox clip.
This is the function to communicate with the outside, e.g. the track.
@ -277,18 +288,19 @@ class Pattern(object):
subdivisions
scale (as tuple so it is hashable)
self._exportCacheVersion
patternLengthMultiplicator
_cachedTransposedScale is updated with self.scale changes and therefore already covered.
"""
cacheHash = (scaleTransposition, halftoneTransposition, howManyUnits, whatTypeOfUnit, subdivisions, tuple(self.scale), self._exportCacheVersion)
cacheHash = (scaleTransposition, halftoneTransposition, howManyUnits, whatTypeOfUnit, subdivisions, tuple(self.scale), self._exportCacheVersion, patternLengthMultiplicator)
try:
return self._builtPatternCache[cacheHash]
except KeyError:
pass
oneMeasureInTicks = howManyUnits * whatTypeOfUnit
oneMeasureInTicks /= subdivisions #subdivisions is 1 by default. bigger values mean shorter values, which is compensated by the user setting bigger howManyUnits manually.
oneMeasureInTicks = int(oneMeasureInTicks)
oneMeasureInTicks /= subdivisions #subdivisions is 1 by default. bigger values mean shorter durations, which is compensated by the user setting bigger howManyUnits manually.
oneMeasureInTicks = int(oneMeasureInTicks) * patternLengthMultiplicator
exportPattern = bytes()

17
engine/track.py

@ -50,6 +50,13 @@ class Track(object): #injection at the bottom of this file!
self.parentData = parentData
self.sequencerInterface = template.engine.sequencer.SequencerInterface(parentTrack=self, name=name) #needs parentData
self.color = color if color else "#00FFFF" # "#rrggbb" in hex. no alpha. a convenience slot for the GUI to save a color.
#2.0
#The distinction between Track and Pattern in our code is artificial, it is the same thing,
#we take inspiration from the GUI that presents the Track on its own.
#The following setting is most likely to be found in the track sub-window:
self.patternLengthMultiplicator = 1 #int. >= 1 the multiplicator is added after all other calculations, like subdivions. We can't integrate this into howManyUnits because that is the global score value
self.pattern = Pattern(parentTrack=self, scale=scale, simpleNoteNames=simpleNoteNames)
self.structure = structure if structure else set() #see buildTrack(). This is the main track data structure besides the pattern. Just integers (starts at 0) as switches which are positions where to play the patterns. In between are automatic rests.
self.whichPatternsAreScaleTransposed = whichPatternsAreScaleTransposed if whichPatternsAreScaleTransposed else {} #position:integers between -7 and 7. Reversed pitch, row based: -7 is higher than 7!!
@ -62,13 +69,15 @@ class Track(object): #injection at the bottom of this file!
def buildTrack(self):
"""The goal is to create a cbox-track, consisting of cbox-clips which hold cbox-pattern,
generated with our own note data. The latter happens in structures_pattern.
This is called after every small change.
"""
#First clean the transpositions of zeroes
self.whichPatternsAreScaleTransposed = {k:v for k,v in self.whichPatternsAreScaleTransposed.items() if v!=0 and k in self.structure}
self.whichPatternsAreHalftoneTransposed = {k:v for k,v in self.whichPatternsAreHalftoneTransposed.items() if v!=0 and k in self.structure}
oneMeasureInTicks = (self.parentData.howManyUnits * self.parentData.whatTypeOfUnit) / self.parentData.subdivisions #subdivisions is 1 by default. bigger values mean shorter values, which is compensated by the user setting bigger howManyUnits manually.
oneMeasureInTicks = int(oneMeasureInTicks)
oneMeasureInTicks = int(oneMeasureInTicks) * self.patternLengthMultiplicator
filteredStructure = [index for index in sorted(self.structure) if index < self.parentData.numberOfMeasures] #not <= because we compare count with range
cboxclips = [o.clip for o in self.sequencerInterface.calfboxTrack.status().clips]
@ -78,7 +87,7 @@ class Track(object): #injection at the bottom of this file!
for index in filteredStructure:
scaleTransposition = self.whichPatternsAreScaleTransposed[index] if index in self.whichPatternsAreScaleTransposed else 0
halftoneTransposition = self.whichPatternsAreHalftoneTransposed[index] if index in self.whichPatternsAreHalftoneTransposed else 0
cboxPattern = self.pattern.buildPattern(scaleTransposition, halftoneTransposition, self.parentData.howManyUnits, self.parentData.whatTypeOfUnit, self.parentData.subdivisions)
cboxPattern = self.pattern.buildPattern(scaleTransposition, halftoneTransposition, self.parentData.howManyUnits, self.parentData.whatTypeOfUnit, self.parentData.subdivisions, self.patternLengthMultiplicator)
r = self.sequencerInterface.calfboxTrack.add_clip(index*oneMeasureInTicks, 0, oneMeasureInTicks, cboxPattern) #pos, pattern-internal offset, length, pattern.
######Old optimisations. Keep for later####
@ -99,6 +108,7 @@ class Track(object): #injection at the bottom of this file!
"pattern" : self.pattern.serialize(),
"whichPatternsAreScaleTransposed" : self.whichPatternsAreScaleTransposed,
"whichPatternsAreHalftoneTransposed" : self.whichPatternsAreHalftoneTransposed,
#"patternLengthMultiplicator" : self.patternLengthMultiplicator,
}
@classmethod
@ -106,6 +116,7 @@ class Track(object): #injection at the bottom of this file!
self = cls.__new__(cls)
self.parentData = parentData
self.sequencerInterface = template.engine.sequencer.SequencerInterface.instanceFromSerializedData(self, serializedData["sequencerInterface"])
#self.patternLengthMultiplicator = serializedData["patternLengthMultiplicator"]
self.color = serializedData["color"]
self.structure = set(serializedData["structure"])
self.whichPatternsAreHalftoneTransposed = {int(k):int(v) for k,v in serializedData["whichPatternsAreHalftoneTransposed"].items()} #json saves dict keys as strings
@ -120,6 +131,8 @@ class Track(object): #injection at the bottom of this file!
"sequencerInterface" : self.sequencerInterface.export(),
"color" : self.color,
"structure" : sorted(self.structure),
"patternBaseLength" : self.parentData.howManyUnits, #for convenient access
"patternLengthMultiplicator" : self.patternLengthMultiplicator, #int
"pattern": self.pattern.exportCache,
"scale": self.pattern.scale,
"simpleNoteNames": self.pattern.simpleNoteNames,

15
qtgui/designer/mainwindow.py

@ -2,9 +2,10 @@
# Form implementation generated from reading ui file 'mainwindow.ui'
#
# Created by: PyQt5 UI code generator 5.13.1
# Created by: PyQt5 UI code generator 5.15.2
#
# WARNING! All changes made in this file will be lost!
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
@ -13,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 619)
MainWindow.resize(1057, 708)
MainWindow.setWindowTitle("Patroneo")
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
@ -44,7 +45,7 @@ class Ui_MainWindow(object):
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.widget_2.sizePolicy().hasHeightForWidth())
self.widget_2.setSizePolicy(sizePolicy)
self.widget_2.setMinimumSize(QtCore.QSize(200, 0))
self.widget_2.setMinimumSize(QtCore.QSize(300, 0))
self.widget_2.setMaximumSize(QtCore.QSize(200, 16777215))
self.widget_2.setObjectName("widget_2")
self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.widget_2)
@ -57,7 +58,7 @@ class Ui_MainWindow(object):
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.widget_3.sizePolicy().hasHeightForWidth())
self.widget_3.setSizePolicy(sizePolicy)
self.widget_3.setMinimumSize(QtCore.QSize(200, 30))
self.widget_3.setMinimumSize(QtCore.QSize(300, 30))
self.widget_3.setMaximumSize(QtCore.QSize(200, 30))
self.widget_3.setObjectName("widget_3")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.widget_3)
@ -92,7 +93,7 @@ class Ui_MainWindow(object):
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.trackEditorView.sizePolicy().hasHeightForWidth())
self.trackEditorView.setSizePolicy(sizePolicy)
self.trackEditorView.setMinimumSize(QtCore.QSize(200, 0))
self.trackEditorView.setMinimumSize(QtCore.QSize(300, 0))
self.trackEditorView.setMaximumSize(QtCore.QSize(200, 16777215))
self.trackEditorView.setFrameShape(QtWidgets.QFrame.NoFrame)
self.trackEditorView.setFrameShadow(QtWidgets.QFrame.Plain)
@ -157,7 +158,7 @@ class Ui_MainWindow(object):
self.toolBar.setObjectName("toolBar")
MainWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 20))
self.menubar.setGeometry(QtCore.QRect(0, 0, 1057, 20))
self.menubar.setObjectName("menubar")
MainWindow.setMenuBar(self.menubar)
self.actionAddTrack = QtWidgets.QAction(MainWindow)

12
qtgui/designer/mainwindow.ui

@ -6,8 +6,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>619</height>
<width>1057</width>
<height>708</height>
</rect>
</property>
<property name="windowTitle">
@ -77,7 +77,7 @@
</property>
<property name="minimumSize">
<size>
<width>200</width>
<width>300</width>
<height>0</height>
</size>
</property>
@ -113,7 +113,7 @@
</property>
<property name="minimumSize">
<size>
<width>200</width>
<width>300</width>
<height>30</height>
</size>
</property>
@ -191,7 +191,7 @@
</property>
<property name="minimumSize">
<size>
<width>200</width>
<width>300</width>
<height>0</height>
</size>
</property>
@ -372,7 +372,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<width>1057</width>
<height>20</height>
</rect>
</property>

5
qtgui/mainwindow.py

@ -181,6 +181,9 @@ class MainWindow(TemplateMainWindow):
pattern because it doesn't deal with patterns. So it didn't receive the new exportDict and
sent its old cached version to the patternGrid via this function. So the grid never
got the new information and "forgot" all settings.
2 years later and I don't understand the docstring anymore.
"""
newCurrentTrackId = exportDict["id"]
@ -195,7 +198,7 @@ class MainWindow(TemplateMainWindow):
pass
d[newCurrentTrackId].mark(True) #New one as active
self.patternGrid.guicallback_chooseCurrentTrack(exportDict)
self.patternGrid.guicallback_chooseCurrentTrack(exportDict, newCurrentTrackId)
#Remember current one for next round and for other functions
#Functions depend on getting set after getting called. They need to know the old track!

61
qtgui/pattern_grid.py

@ -64,6 +64,8 @@ class PatternGrid(QtWidgets.QGraphicsScene):
self._steps = {} # (x,y):Step()
self._labels = [] #Step numbers
self._tracks = {} #tr-id:exportDict #kept up to date by various callbacks.
self._zoomFactor = 1 # no save. We don't keep a qt config.
#Set color, otherwise it will be transparent in window managers or wayland that want that.
@ -94,15 +96,24 @@ class PatternGrid(QtWidgets.QGraphicsScene):
api.callbacks.patternChanged.append(self.callback_patternChanged)
api.callbacks.trackMetaDataChanged.append(self.callback_trackMetaDataChanged)
api.callbacks.subdivisionsChanged.append(self.guicallback_subdivisionsChanged)
api.callbacks.patternLengthMultiplicatorChanged.append(self.callback_patternLengthMultiplicatorChanged)
def callback_patternLengthMultiplicatorChanged(self, exportDict):
self._tracks[exportDict["id"]] = exportDict
self._fullRedraw(exportDict["id"])
def callback_timeSignatureChanged(self, howMany, typeInTicks):
"""The typeInTicks actually changes nothing visually here.
"""
This is the global base timesig, without patternLengthMultiplicator.
The typeInTicks actually changes nothing visually here.
We only care about howMany steps we offer."""
self.oneMeasureInTicks = howMany * typeInTicks
self.ticksToPixelRatio = typeInTicks / SIZE_UNIT
self._redrawSteps(howMany)
def _redrawSteps(self, howMany):
def _redrawSteps(self, howMany, forceId=None):
"""Draw the empty steps grid. This only happens if the pattern itself changes,
for example with the time signature or with a GUI subdivision change.
Normal step on/off is done incrementally.
@ -111,8 +122,16 @@ class PatternGrid(QtWidgets.QGraphicsScene):
self.removeItem(existingStep)
self._steps = {} # (x,y):Step()
if not self.parentView.parentMainWindow.currentTrackId or not self.parentView.parentMainWindow.currentTrackId in self._tracks:
factor = 1 #program start
else:
if forceId:
factor = self._tracks[forceId]["patternLengthMultiplicator"]
else:
factor = self._tracks[self.parentView.parentMainWindow.currentTrackId]["patternLengthMultiplicator"]
#Build a two dimensional grid
for column in range(howMany):
for column in range(howMany*factor):
for row in range(api.NUMBER_OF_STEPS):
x = column * SIZE_UNIT + SIZE_RIGHT_OFFSET
y = row * SIZE_UNIT + SIZE_TOP_OFFSET
@ -127,23 +146,33 @@ class PatternGrid(QtWidgets.QGraphicsScene):
h = step.y() + SIZE_UNIT + SIZE_TOP_OFFSET + SIZE_BOTTOM_OFFSET #same as w
self.setSceneRect(0, 0, w, h)
def guicallback_chooseCurrentTrack(self, exportDict):
"""It is guaranteed that this only happens on a real track change, not twice the same.
During the track change the currenTrackId value is still the old
track. There we introduce a force switch that enables self.guicallback_chooseCurrentTrack
to trigger a redraw even during the track change.
"""
assert not exportDict["id"] == self.parentView.parentMainWindow.currentTrackId #this is still the old track.
def _fullRedraw(self, trackId):
"""Uses cached values"""
exportDict = self._tracks[trackId]
updateMode = self.parentView.viewportUpdateMode() #prevent iteration flickering from color changes.
self.parentView.setViewportUpdateMode(self.parentView.NoViewportUpdate)
self._redrawSteps(exportDict["patternBaseLength"], forceId=trackId)
self.callback_trackMetaDataChanged(exportDict, force=True) #we need the color when setting pattern changed. This needs to be called before patternChanged
self.callback_patternChanged(exportDict, force=True) #needs to be called after trackMetaDataChanged for the color.
self.guicallback_subdivisionsChanged(self._cacheSubdivisonValue)
self.removeShadows()
self.parentView.setViewportUpdateMode(updateMode)
def guicallback_chooseCurrentTrack(self, exportDict, newCurrentTrackId):
"""It is guaranteed that this only happens on a real track change, not twice the same.
During the track change the currenTrackId value is still the old
track. There we introduce a force switch that enables self.guicallback_chooseCurrentTrack
to trigger a redraw even during the track change.
"""
assert not exportDict["id"] == self.parentView.parentMainWindow.currentTrackId #this is still the old track.
self._tracks[exportDict["id"]] = exportDict
self._fullRedraw(newCurrentTrackId)
def callback_patternChanged(self, exportDict, force=False):
"""We receive the whole track as exportDict.
@ -156,6 +185,8 @@ class PatternGrid(QtWidgets.QGraphicsScene):
to trigger a redraw even during the track change.
"""
self._tracks[exportDict["id"]] = exportDict
if force or exportDict["id"] == self.parentView.parentMainWindow.currentTrackId:
updateMode = self.parentView.viewportUpdateMode() #prevent iteration flickering from color changes.
self.parentView.setViewportUpdateMode(self.parentView.NoViewportUpdate)
@ -163,6 +194,7 @@ class PatternGrid(QtWidgets.QGraphicsScene):
for step in self._steps.values():
step.off()
for noteDict in exportDict["pattern"]:
x = noteDict["index"]
y = noteDict["pitch"]
@ -191,6 +223,8 @@ class PatternGrid(QtWidgets.QGraphicsScene):
to trigger a redraw even during the track change.
"""
self._tracks[exportDict["id"]] = exportDict
if force or self.parentView.parentMainWindow.currentTrackId == exportDict["id"]:
self.trackName.setText(exportDict["sequencerInterface"]["name"])
self.trackName.show()
@ -210,13 +244,15 @@ class PatternGrid(QtWidgets.QGraphicsScene):
def guicallback_subdivisionsChanged(self, newValue):
"""handle measuresPerGroup"""
self._cacheSubdivisonValue = newValue
#Draw labels
for existinglabel in self._labels:
self.removeItem(existinglabel)
self._labels = []
for (x,y), step in self._steps.items():
step.main = not x % newValue
step.main = not x % newValue # what magic is this?
step.setApperance()
groupCounter, beatNumber = divmod(x, newValue)
if not beatNumber:
@ -857,7 +893,8 @@ class Playhead(QtWidgets.QGraphicsLineItem):
def setCursorPosition(self, tickindex, playbackStatus):
"""Using modulo makes the playback cursor wrap around and play over the pattern
eventhough we use the global tick value."""
x = (tickindex % self.parentScene.oneMeasureInTicks) / self.parentScene.ticksToPixelRatio
factor = self.parentScene._tracks[self.parentScene.parentView.parentMainWindow.currentTrackId]["patternLengthMultiplicator"]
x = (tickindex % (self.parentScene.oneMeasureInTicks * factor)) / self.parentScene.ticksToPixelRatio
x += SIZE_RIGHT_OFFSET
if playbackStatus: # api.duringPlayback:
self.show()

160
qtgui/songeditor.py

@ -23,6 +23,8 @@ 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.
import template.qtgui.helper as helper
from PyQt5 import QtCore, QtGui, QtWidgets
SIZE_UNIT = 25 #this is in manual sync with timeline.py SIZE_UNIT
@ -30,8 +32,9 @@ SIZE_TOP_OFFSET = 0
_zValuesRelativeToScene = { #Only use for objects added directly to the scene, not children.
"trackStructure":3,
"switch":4,
"barline":5,
"switch":6,
"barlineGroupHighlight":8,
"playhead":90,
}
@ -44,6 +47,8 @@ class SongEditor(QtWidgets.QGraphicsScene):
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)
@ -51,6 +56,7 @@ class SongEditor(QtWidgets.QGraphicsScene):
self.tracks = {} #TrackID:TrackStructures
self.barlines = [] #in order
self.cachedCombinedTrackHeight = 0 #set by callback_tracksChanged
self.trackOrder = [] #contains engine-ids, set by callback_numberOfTracksChanged
@ -66,6 +72,7 @@ class SongEditor(QtWidgets.QGraphicsScene):
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
@ -135,6 +142,7 @@ class SongEditor(QtWidgets.QGraphicsScene):
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)
@ -170,12 +178,22 @@ class SongEditor(QtWidgets.QGraphicsScene):
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)
class TrackStructure(QtWidgets.QGraphicsRectItem):
"""From left to right. Holds two lines to show the "staffline" and a number of switches,
@ -184,6 +202,7 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
def __init__(self, parentScene):
super().__init__(0,0,1,SIZE_UNIT)
self.parentScene = parentScene
self.setAcceptHoverEvents(True) #for the preview highlight switch
self.exportDict = None #self.update gets called immediately after creation.
self.switches = {} # position:switchInstance
@ -210,10 +229,21 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
self._markerLine.setZValue(_zValuesRelativeToScene["playhead"])
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)
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:
@ -230,11 +260,14 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
if not position in self.switches:
self.switches[position] = self._createSwitch(position)
self.updateSwitchVisibility(exportDict["numberOfMeasures"])
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():
@ -242,7 +275,18 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
switch.setScaleTransposeColor(self.labelColor)
switch.setHalftoneTransposeColor(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)
@ -260,8 +304,13 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
switch = Switch(parentTrackStructure=self, position=position)
assert self.currentColor
switch.setBrush(self.currentColor)
switch.setParentItem(self)
switch.setX(position * SIZE_UNIT)
#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):
@ -272,12 +321,26 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
structure = self.exportDict["structure"]
whichPatternsAreScaleTransposed = self.exportDict["whichPatternsAreScaleTransposed"]
whichPatternsAreHalftoneTransposed = self.exportDict["whichPatternsAreHalftoneTransposed"]
factor = self.exportDict["patternLengthMultiplicator"]
#Adjust highlight mouse hover to new stretch factor
r = self._highlightSwitch.rect()
r.setRight(SIZE_UNIT * factor)
self._highlightSwitch.setRect(r)
for position, switch in self.switches.items():
if position < requestAmountOfMeasures and position in structure:
switch.show()
else:
#Deal with measures that stretch multiple base measures
switch.stretch(factor)
switch.setPos(position * SIZE_UNIT * factor, self.y())
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 end 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
@ -290,7 +353,9 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
switch.halftoneTransposeOff()
def scenePos2switchPosition(self, x):
return int(x / SIZE_UNIT)
"""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.
@ -314,7 +379,6 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
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.
@ -348,6 +412,8 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
menu.exec_(pos)
def mouseMoveEvent(self, event):
"""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
if self._mousePressOn and position != self._mousePressOn[2]:
#self._markerLine.setLine(0,0, (position - self._mousePressOn[2])*SIZE_UNIT + SIZE_UNIT/2, 0)
@ -371,6 +437,22 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
else:
self._markerLine.hide()
def hoverEnterEvent(self, event):
self._highlightSwitch.show()
#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)
def hoverLeaveEvent(self, event):
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:
@ -400,15 +482,18 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
class Switch(QtWidgets.QGraphicsRectItem):
"""Switches live for the duration of the track. Once created they only ever get hidden/shown,
never deleted."""
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.position = position
super().__init__(0,0,SIZE_UNIT,SIZE_UNIT)
self.setAcceptHoverEvents(True)
#self.rect().setWidth(SIZE_UNIT)
self.setZValue(_zValuesRelativeToScene["switch"])
self.setAcceptHoverEvents(True)
self.scaleTransposeGlyph = QtWidgets.QGraphicsSimpleTextItem("")
self.scaleTransposeGlyph.setParentItem(self)
@ -426,6 +511,13 @@ class Switch(QtWidgets.QGraphicsRectItem):
self.halftoneTransposeGlyph.hide()
self.halftoneTranspose = 0
def stretch(self, factor):
"""factor assumes relative to SIZE_UNIT"""
r = self.rect()
r.setRight(SIZE_UNIT * factor)
self.setRect(r)
def setScaleTranspose(self, value):
"""
Called by track callbacks and also for the temporary buffer display
@ -518,15 +610,15 @@ class Switch(QtWidgets.QGraphicsRectItem):
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.tracks = {} #TrackID:TrackLabel
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)
@ -535,7 +627,10 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene):
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):
@ -545,6 +640,9 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene):
"""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):
toDelete = set(self.tracks.keys())
@ -575,6 +673,9 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene):
self.cachedCombinedTrackHeight = len(self.tracks) * SIZE_UNIT
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 contextMenuEvent(self, event):
"""
We can't delete this properly object from within. The engine callback will react faster
@ -650,24 +751,46 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
super().__init__(0, 0, width, height)
self.parentScene = parentScene
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.positioningHandle.setPos(0,0)
self.positioningHandle.setToolTip(QtCore.QCoreApplication.translate("TrackLabel", "grab and move to reorder tracks"))
self.lengthMultiplicatorSpinBox = TrackLabel.lengthMultiplicatorSpinBox(parentTrackLabel=self)
self.lengthMultiplicatorSpinBox.setParentItem(self)
self.lengthMultiplicatorSpinBox.setPos(SIZE_UNIT,0)
self.colorButton = TrackLabel.ColorPicker(parentTrackLabel=self)
self.colorButton.setParentItem(self)
self.colorButton.setPos(SIZE_UNIT, 3)
self.colorButton.setPos(4*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.label.setPos(5*SIZE_UNIT+3,0)
class lengthMultiplicatorSpinBox(QtWidgets.QGraphicsProxyWidget):
def __init__(self, parentTrackLabel):
super().__init__()
self.parentTrackLabel = parentTrackLabel
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 spinBoxValueChanged(self):
self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict)
api.setTrackPatternLengthMultiplicator(self.parentTrackLabel.exportDict["id"], self.spinBox.value())
self.setFlag(self.ItemIgnoresTransformations)
class ColorPicker(QtWidgets.QGraphicsRectItem):
def __init__(self, parentTrackLabel):
@ -783,9 +906,10 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
def update(self, exportDict):
self.lineEdit.setText(exportDict["sequencerInterface"]["name"])
self.exportDict = exportDict
self.lineEdit.setText(exportDict["sequencerInterface"]["name"])
self.colorButton.setBrush(QtGui.QColor(exportDict["color"]))
self.lengthMultiplicatorSpinBox.spinBox.setValue(int(exportDict["patternLengthMultiplicator"]))
def mousePressEvent(self,event):
self.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.exportDict)

Loading…
Cancel
Save