diff --git a/CHANGELOG b/CHANGELOG
index 507df52..e6562b1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,4 +1,7 @@
2019-10-15 Version 1.4
+New context option for track labels: Replace pattern with one from other track
+Streamline existing context menu options for measure groups
+New context options for measure group: duplicate and clean transpositions
Activate the first pattern for all three tracks on a new project to ease up starting
Set default drums track for new projects to use GM Drum note names and start with a good scale
Use Median Velocity for new notes, not average.
diff --git a/engine/api.py b/engine/api.py
index 2f98a74..43783eb 100644
--- a/engine/api.py
+++ b/engine/api.py
@@ -171,7 +171,7 @@ from template.engine.api import callbacks
_templateStartEngine = startEngine
def updatePlayback():
- #TODO: use template.sequencer.py internal updates instead
+ #TODO: use template.sequencer.py internal updates instead
cbox.Document.get_song().update_playback()
def startEngine(nsmClient):
@@ -232,7 +232,7 @@ def seek(value):
cbox.Transport.seek_ppqn(value)
##Score
-def set_quarterNotesPerMinute(value:int):
+def set_quarterNotesPerMinute(value):
if value is None:
session.data.tempoMap.isTransportMaster = False #triggers rebuild
elif value == "on":
@@ -242,7 +242,7 @@ def set_quarterNotesPerMinute(value:int):
else:
assert value > 0
session.data.tempoMap.setQuarterNotesPerMinute(value)
- session.data.tempoMap.isTransportMaster = True #triggers rebuild
+ session.data.tempoMap.isTransportMaster = True #triggers rebuild
#Does not need track rebuilding
updatePlayback()
callbacks._quarterNotesPerMinuteChanged()
@@ -419,6 +419,18 @@ def trackMergeCopyFrom(sourceTrackId, targetTrackId):
targetTrack.buildTrack()
updatePlayback()
callbacks._trackStructureChanged(targetTrack)
+
+def trackPatternReplaceFrom(sourceTrackId, targetTrackId):
+ if not sourceTrackId == targetTrackId:
+ sourceTrack = session.data.trackById(sourceTrackId)
+ targetTrack = session.data.trackById(targetTrackId)
+
+ copyPattern = sourceTrack.pattern.copy(newParentTrack = targetTrack)
+ targetTrack.pattern = copyPattern
+
+ targetTrack.buildTrack()
+ updatePlayback()
+ callbacks._patternChanged(targetTrack)
def setSwitchScaleTranspose(trackId, position, transpose):
"""Scale transposition is flipped. lower value means higher pitch"""
@@ -448,6 +460,33 @@ def insertSilence(howMany, beforeMeasureNumber):
session.data.buildAllTracks()
updatePlayback()
+def duplicateSwitchGroup(startMeasureForGroup:int, endMeasureExclusive:int):
+ groupSize = endMeasureExclusive-startMeasureForGroup
+ insertSilence(groupSize, endMeasureExclusive)
+
+ for track in session.data.tracks:
+ for switch in range(startMeasureForGroup+groupSize, endMeasureExclusive+groupSize): #One group after the given one.
+ if switch-groupSize in track.structure:
+ track.structure.add(switch)
+ if switch-groupSize in track.whichPatternsAreScaleTransposed:
+ track.whichPatternsAreScaleTransposed[switch] = track.whichPatternsAreScaleTransposed[switch-groupSize]
+ if switch-groupSize in track.whichPatternsAreHalftoneTransposed:
+ track.whichPatternsAreHalftoneTransposed[switch] = track.whichPatternsAreHalftoneTransposed[switch-groupSize]
+ callbacks._trackStructureChanged(track)
+ session.data.buildAllTracks()
+ updatePlayback()
+
+def clearSwitchGroupTranspositions(startMeasureForGroup:int, endMeasureExclusive:int):
+ for track in session.data.tracks:
+ for switch in range(startMeasureForGroup, endMeasureExclusive):
+ if switch in track.whichPatternsAreScaleTransposed:
+ del track.whichPatternsAreScaleTransposed[switch]
+ if switch in track.whichPatternsAreHalftoneTransposed:
+ del track.whichPatternsAreHalftoneTransposed[switch]
+ callbacks._trackStructureChanged(track)
+ session.data.buildAllTracks()
+ updatePlayback()
+
def deleteSwitches(howMany, fromMeasureNumber):
for track in session.data.tracks:
new_structure = set()
diff --git a/engine/pattern.py b/engine/pattern.py
index 1e8ba5d..df8d75a 100644
--- a/engine/pattern.py
+++ b/engine/pattern.py
@@ -59,7 +59,7 @@ class Pattern(object):
self.parentTrack = parentTrack
self.scale = scale if scale else (72, 71, 69, 67, 65, 64, 62, 60) #Scale needs to be set first because on init/load data already depends on it, at least the default scale. The scale is part of the track meta callback.
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
+ self.simpleNoteNames = simpleNoteNames if simpleNoteNames else self.parentTrack.parentData.lastUsedNotenames[:] #This is mostly for the GUI or other kinds of representation instead midi notes
self._processAfterInit()
def _prepareBeforeInit(self):
@@ -75,6 +75,14 @@ class Pattern(object):
self._builtPatternCache = {} #holds a ready cbox pattern for a clip as value. Key is a tuple of hashable parameters. see self.buildPattern
+ def copy(self, newParentTrack):
+ """Return an independent copy of this pattern"""
+ data = [note.copy() for note in self.data] #list of mutable dicts. Dicts have only primitve data types inside
+ scale = self.scale #it is immutable so there is no risk of changing it in place for both patterns at once
+ simpleNoteNames = self.simpleNoteNames[:] #this mutable list always gets replaced completely by setting a new list, but we don't want to take any chances and create a slice copy.
+ result = Pattern(newParentTrack, data, scale, simpleNoteNames)
+ return result
+
@property
def scale(self):
return self._scale
diff --git a/qtgui/resources/translations/de.qm b/qtgui/resources/translations/de.qm
index 2bd3ed0..b83eae6 100644
Binary files a/qtgui/resources/translations/de.qm and b/qtgui/resources/translations/de.qm differ
diff --git a/qtgui/resources/translations/de.ts b/qtgui/resources/translations/de.ts
index 848d9cc..6902f52 100644
--- a/qtgui/resources/translations/de.ts
+++ b/qtgui/resources/translations/de.ts
@@ -194,12 +194,32 @@
- {} leere Takte vor Takt {} einfügen
+ {} leere Takte vor Takt {} einfügen
- Lösche {} Takte von Takt {} beginnend
+ Lösche {} Takte von Takt {} beginnend
+
+
+
+
+ Leere Taktgruppe vor dieser einfügen
+
+
+
+
+ Lösche diese Taktgruppe
+
+
+
+
+ Verdopple diese Taktgruppe inkl. Struktur
+
+
+
+
+ Setze alle Transpositionen dieser Taktgruppe zurück
@@ -324,12 +344,12 @@
TrackLabel
-
+ mit der maus halten und ziehen um Spuren anzuordnen
-
+ setze Farbe der Spur
@@ -337,34 +357,44 @@
TrackLabelContext
-
+ Taktauswahl umdrehen
-
+ Alle Takte anschalten
-
+ Alle Takte ausschalten
-
+ Spur klonen
-
+ Spur löschen
- Übernimm Struktur von
+ Übernimm Struktur von
+
+
+
+
+ Übernimm und ergänze Struktur von
+
+
+
+
+ Ersetze Noten des Taktes durch
diff --git a/qtgui/songeditor.py b/qtgui/songeditor.py
index 514b949..c3093bc 100644
--- a/qtgui/songeditor.py
+++ b/qtgui/songeditor.py
@@ -172,7 +172,7 @@ class SongEditor(QtWidgets.QGraphicsScene):
barline.setPen(self.normalPen)
class TrackStructure(QtWidgets.QGraphicsRectItem):
- """From left to right. Holds two lines to show the "stafflinen" and a number of switches,
+ """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):
@@ -320,10 +320,15 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based
measuresPerGroup = self.parentScene.measuresPerGroupCache
+ offset = position % measuresPerGroup
+ startMeasureForGroup = position - offset
+ endMeasureExclusive = startMeasureForGroup + measuresPerGroup
+
listOfLabelsAndFunctions = [
-
- (QtCore.QCoreApplication.translate("SongStructure", "Insert {} empty measures before no. {}").format(measuresPerGroup, position+1), lambda: api.insertSilence(howMany=measuresPerGroup, beforeMeasureNumber=position)),
- (QtCore.QCoreApplication.translate("SongStructure", "Delete {} measures from no. {} on").format(measuresPerGroup, position+1), lambda: api.deleteSwitches(howMany=measuresPerGroup, fromMeasureNumber=position)),
+ (QtCore.QCoreApplication.translate("SongStructure", "Insert empty group before this one").format(measuresPerGroup), lambda: api.insertSilence(howMany=measuresPerGroup, beforeMeasureNumber=startMeasureForGroup)),
+ (QtCore.QCoreApplication.translate("SongStructure", "Delete whole group").format(measuresPerGroup), lambda: api.deleteSwitches(howMany=measuresPerGroup, fromMeasureNumber=startMeasureForGroup)),
+ (QtCore.QCoreApplication.translate("SongStructure", "Duplicate whole group including measures"), lambda: api.duplicateSwitchGroup(startMeasureForGroup, endMeasureExclusive)),
+ (QtCore.QCoreApplication.translate("SongStructure", "Clear all group transpositions"), lambda: api.clearSwitchGroupTranspositions(startMeasureForGroup, endMeasureExclusive)),
]
for text, function in listOfLabelsAndFunctions:
@@ -592,8 +597,8 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene):
a.triggered.connect(function)
#Add a submenu for merge/copy
- mergeMenu = menu.addMenu(QtCore.QCoreApplication.translate("TrackLabelContext", "Merge/Copy from"))
-
+ mergeMenu = menu.addMenu(QtCore.QCoreApplication.translate("TrackLabelContext", "Merge/Copy Measure-Structure from"))
+
def createCopyMergeLambda(srcId):
return lambda: api.trackMergeCopyFrom(srcId, exportDict["id"])
@@ -606,6 +611,21 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene):
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 track in self.tracks.values():
+ sourceDict = track.exportDict
+ a = QtWidgets.QAction(sourceDict["sequencerInterface"]["name"], copyMenu)
+ copyMenu.addAction(a)
+ mergeCommand = replacePatternWithLambda(sourceDict["id"])
+ if sourceDict["id"] == exportDict["id"]:
+ a.setEnabled(False)
+ a.triggered.connect(mergeCommand)
+
pos = QtGui.QCursor.pos()
pos.setY(pos.y() + 5)
self.parentView.parentMainWindow.setFocus()