Nils
6 years ago
9 changed files with 2922 additions and 198 deletions
@ -0,0 +1,127 @@ |
|||
#! /usr/bin/env python3 |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
Copyright 2018, Nils Hilbricht, Germany ( https://www.hilbricht.net ) |
|||
|
|||
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), |
|||
more specifically its template base application. |
|||
|
|||
The Template Base Application 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/>. |
|||
""" |
|||
|
|||
#Standard Library Modules |
|||
|
|||
#Third Party Modules |
|||
|
|||
#Template Modules |
|||
import template.engine.sequencer |
|||
|
|||
#Our modules |
|||
|
|||
class Track(object): #injection at the bottom of this file! |
|||
"""The pattern is same as the track, even if the GUI does not represent it that way""" |
|||
|
|||
def __repr__(self) -> str: |
|||
return f"Patroneo Track: {self.name}" |
|||
|
|||
#def __init__(self, parentScore, name="", structure=None, pattern=None, scale=None, color=None, whichPatternsAreScaleTransposed=None, whichPatternsAreHalftoneTransposed=None, noteNames=None): |
|||
def __init__(self, parentScore, name="", structure=None, pattern=None, scale=None, color=None, whichPatternsAreScaleTransposed=None, whichPatternsAreHalftoneTransposed=None, noteNames=None): |
|||
|
|||
print (self) |
|||
|
|||
self.color = color if color else "#00FFFF" # "#rrggbb" in hex. no alpha. a convenience slot for the GUI to save a color. |
|||
|
|||
#User data: |
|||
self.pattern = pattern if pattern else Pattern(parentTrack = self, scale=scale, noteNames=noteNames) |
|||
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. |
|||
|
|||
if whichPatternsAreScaleTransposed: |
|||
self.whichPatternsAreScaleTransposed = {int(k):int(v) for k,v in whichPatternsAreScaleTransposed.items()} #json saves dict keys as strings |
|||
else: |
|||
self.whichPatternsAreScaleTransposed = {} #position:integers between -7 and 7. Reversed pitch, row based: -7 is higher than 7!! |
|||
|
|||
if whichPatternsAreHalftoneTransposed: |
|||
self.whichPatternsAreHalftoneTransposed = {int(k):int(v) for k,v in whichPatternsAreHalftoneTransposed.items()} #json saves dict keys as strings |
|||
else: |
|||
self.whichPatternsAreHalftoneTransposed = {} #position:integers between -7 and 7. Reversed pitch, row based: -7 is higher than 7!! |
|||
|
|||
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. |
|||
""" |
|||
#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.parentScore.howManyUnits * self.parentScore.whatTypeOfUnit) / self.parentScore.subdivisions #subdivisions is 1 by default. bigger values mean shorter values, which is compensated by the user setting bigger howManyUnits manually. |
|||
oneMeasureInTicks = int(oneMeasureInTicks) |
|||
|
|||
filteredStructure = [index for index in sorted(self.structure) if index < self.parentScore.numberOfMeasures] #not <= because we compare count with range |
|||
cboxclips = [o.clip for o in self.calfboxTrack.status().clips] |
|||
|
|||
for cboxclip in cboxclips: |
|||
cboxclip.delete() #removes itself from the track |
|||
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.parentScore.howManyUnits, self.parentScore.whatTypeOfUnit, self.parentScore.subdivisions) |
|||
r = self.calfboxTrack.add_clip(index*oneMeasureInTicks, 0, oneMeasureInTicks, cboxPattern) #pos, pattern-internal offset, length, pattern. |
|||
|
|||
######Old optimisations. Keep for later#### |
|||
########################################## |
|||
#if changeClipsInPlace: #no need for track.buildTrack. Very cheap pattern exchange. |
|||
# cboxclips = [o.clip for o in self.parentTrack.calfboxTrack.status().clips] |
|||
# for cboxclip in cboxclips: |
|||
# cboxclip.set_pattern(self.cboxPattern[cboxclip.patroneoScaleTransposed]) |
|||
|
|||
|
|||
def serialize(self)->dict: |
|||
dictionary = super().serialize() |
|||
dictionary.update( { #update in place |
|||
"color" : self.color, |
|||
"structure" : list(self.structure), |
|||
"data" : self.pattern.data, |
|||
"scale" : self.pattern.scale, #The scale is part of the track meta callback. |
|||
"noteNames" : self.pattern.noteNames, #The noteNames are part of the track meta callback. |
|||
"whichPatternsAreScaleTransposed" : self.whichPatternsAreScaleTransposed, |
|||
"whichPatternsAreHalftoneTransposed" : self.whichPatternsAreHalftoneTransposed, |
|||
}) |
|||
return dictionary |
|||
|
|||
@classmethod |
|||
def instanceFromSerializedData(cls, parentTrack, serializedData): |
|||
self = cls.__new__(cls) |
|||
self._name = serializedData["name"] |
|||
self.parentTrack = parentTrack |
|||
self.parentScore = parentTrack.parentScore |
|||
|
|||
self._processAfterInit() |
|||
return self |
|||
|
|||
def export(self)->dict: |
|||
dictionary = super().export() |
|||
dictionary.update({ |
|||
"color" : self.color, |
|||
"structure" : sorted(self.structure), |
|||
"pattern": self.pattern.exportCache, |
|||
"scale": self.pattern.scale, |
|||
"noteNames": self.pattern.noteNames, |
|||
"numberOfMeasures": self.parentScore.numberOfMeasures, |
|||
"whichPatternsAreScaleTransposed": self.whichPatternsAreScaleTransposed, |
|||
"whichPatternsAreHalftoneTransposed": self.whichPatternsAreHalftoneTransposed, |
|||
|
|||
}) |
|||
|
|||
#Dependency Injections. |
|||
template.engine.sequencer.Score.TrackClass = Track #Score will look for Track in its module. |
@ -0,0 +1,882 @@ |
|||
#! /usr/bin/env python3 |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
Copyright 2018, Nils Hilbricht, Germany ( https://www.hilbricht.net ) |
|||
|
|||
This file is part of Patroneo ( https://www.laborejo.org ) |
|||
|
|||
Laborejo is free software: you can redistribute it and/or modify |
|||
it under the terms of the GNU General Public License as published by |
|||
the Free Software Foundation, either version 3 of the License, or |
|||
(at your option) any later version. |
|||
|
|||
This program is distributed in the hope that it will be useful, |
|||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
GNU General Public License for more details. |
|||
|
|||
You should have received a copy of the GNU General Public License |
|||
along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
""" |
|||
from time import time |
|||
import engine.api as api #Session is already loaded and created, no duplication. |
|||
from template.engine import pitch |
|||
from PyQt5 import QtCore, QtGui, QtWidgets |
|||
|
|||
SIZE_UNIT = 40 |
|||
SIZE_TOP_OFFSET = 75 |
|||
SIZE_BOTTOM_OFFSET = 35 |
|||
SIZE_RIGHT_OFFSET = 80 |
|||
|
|||
_zValuesRelativeToScene = { #Only use for objects added directly to the scene, not children. |
|||
"off" : 1, |
|||
"shadow" : 4, |
|||
"step" :5, |
|||
"scale" : 20, #so the drop down menu is above the steps |
|||
} |
|||
|
|||
class PatternGrid(QtWidgets.QGraphicsScene): |
|||
""" |
|||
data example for c'4 d'8 e' f'2 in a 4/4 timesig. Actually in any timesig. |
|||
[ |
|||
{"index:0", "pitch": 60, "factor": 1 , "velocity":110}, |
|||
{"index:1", "pitch": 62, "factor": 0.5 , "velocity":90}, |
|||
{"index:1.5", "pitch": 64, "factor": 0.5 , "velocity":80}, |
|||
{"index:2", "pitch": 65, "factor": 2 , "velocity":60}, |
|||
] |
|||
|
|||
We delete most of our content and redraw if the timesignature changes. |
|||
|
|||
We draw all steps at once, even if hidden. |
|||
|
|||
If the active track changes we only change the status (color) of steps but not the |
|||
steps themselves. We do not save any track state here but always react dynamically |
|||
and sent every change we do ourselves simply with the currentTrackId |
|||
|
|||
""" |
|||
|
|||
def __init__(self, parentView): |
|||
super().__init__() |
|||
self.parentView = parentView |
|||
self._steps = {} # (x,y):Step() |
|||
self._labels = [] #Step numbers |
|||
|
|||
self._zoomFactor = 1 # no save. We don't keep a qt config. |
|||
|
|||
role = QtGui.QPalette.BrightText |
|||
self.textColor = self.parentView.parentMainWindow.fPalBlue.color(role) |
|||
self.labelColor = QtGui.QColor("black") #save for new step |
|||
|
|||
self.trackName = QtWidgets.QGraphicsSimpleTextItem("") |
|||
self.trackName.setBrush(self.textColor) |
|||
self.addItem(self.trackName) |
|||
self.trackName.setPos(0,0) |
|||
|
|||
self.scale = Scale(parentScene=self) |
|||
self.addItem(self.scale) |
|||
self.scale.setPos(0, SIZE_TOP_OFFSET) |
|||
|
|||
velocityControlsProxy = self.addWidget(VelocityControls(parentScene=self)) |
|||
velocityControlsProxy.setPos(0, 25) #we can't get the height of the track name properly. So it was trial and error... |
|||
velocityControlsProxy.setZValue(_zValuesRelativeToScene["scale"]) #so the drop down menu is above the steps |
|||
|
|||
tranposeControlsProxy = self.addWidget(TransposeControls(parentScene=self)) |
|||
tranposeControlsProxy.setPos(velocityControlsProxy.geometry().width() + 10, 25) #we can't get the height of the track name properly. So it was trial and error... |
|||
tranposeControlsProxy.setZValue(_zValuesRelativeToScene["scale"]) #so the drop down menu is above the steps |
|||
|
|||
self._middleMouseDown = False |
|||
|
|||
#self.ticksToPixelRatio set by callback_timeSignatureChanged |
|||
self.playhead = Playhead(parentScene = self) |
|||
self.addItem(self.playhead) |
|||
self.playhead.setY(SIZE_TOP_OFFSET) |
|||
|
|||
api.callbacks.timeSignatureChanged.append(self.callback_timeSignatureChanged) |
|||
api.callbacks.patternChanged.append(self.callback_patternChanged) |
|||
api.callbacks.trackMetaDataChanged.append(self.callback_trackMetaDataChanged) |
|||
api.callbacks.subdivisionsChanged.append(self.guicallback_subdivisionsChanged) |
|||
|
|||
def callback_timeSignatureChanged(self, howMany, typeInTicks): |
|||
"""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): |
|||
"""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. |
|||
""" |
|||
for existingStep in self._steps.values(): |
|||
self.removeItem(existingStep) |
|||
self._steps = {} # (x,y):Step() |
|||
|
|||
#Build a two dimensional grid |
|||
for column in range(howMany): |
|||
for row in range(api.NUMBER_OF_STEPS): |
|||
x = column * SIZE_UNIT + SIZE_RIGHT_OFFSET |
|||
y = row * SIZE_UNIT + SIZE_TOP_OFFSET |
|||
|
|||
step = Step(parentScene=self, column=column, row=row) |
|||
step.setPos(x, y) |
|||
self.addItem(step) |
|||
self._steps[(column, row)] = step |
|||
|
|||
#there is always at least one column so we don't need to try step for AttributeError |
|||
w = step.x() + SIZE_UNIT + SIZE_RIGHT_OFFSET #the position of the last step plus one step width and one offset more for good measure |
|||
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. |
|||
|
|||
#self.trackName.setText(exportDict["name"]) |
|||
|
|||
updateMode = self.parentView.viewportUpdateMode() #prevent iteration flickering from color changes. |
|||
self.parentView.setViewportUpdateMode(self.parentView.NoViewportUpdate) |
|||
|
|||
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.removeShadows() |
|||
|
|||
self.parentView.setViewportUpdateMode(updateMode) |
|||
|
|||
|
|||
def callback_patternChanged(self, exportDict, force=False): |
|||
"""We receive the whole track as exportDict. |
|||
exportDict["pattern"] is the data structure example in the class docstring. |
|||
|
|||
We also receive this for every track, no matter if this our current working track. |
|||
So we check if we are the current track. However, that prevents setting up or steps |
|||
on a track change because 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. |
|||
""" |
|||
|
|||
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) |
|||
|
|||
for step in self._steps.values(): |
|||
step.off() |
|||
|
|||
for noteDict in exportDict["pattern"]: |
|||
x = noteDict["index"] |
|||
y = noteDict["pitch"] |
|||
velocityAndFactor = (noteDict["velocity"], noteDict["factor"]) |
|||
self._steps[(x,y)].on(velocityAndFactor=velocityAndFactor, exceedsPlayback=noteDict["exceedsPlayback"]) |
|||
|
|||
self.scale.setScale(exportDict["scale"]) |
|||
self.scale.setNoteNames(exportDict["noteNames"]) |
|||
|
|||
self.parentView.setViewportUpdateMode(updateMode) |
|||
|
|||
#else ignore. We fetch new data when we change the track anyway. |
|||
|
|||
|
|||
|
|||
|
|||
#Deprectated. We do incremental updates now. But who knows if we need it in the future. I doubt it... |
|||
#def sendCurrentPatternToEngine(self): |
|||
# pattern = [step.export() for step in self._steps.values() if step.status] #engine compatible backend dict of the current GUI state. Send the switched on values. |
|||
# api.setPattern(trackId=self.parentView.parentMainWindow.currentTrackId, patternList=pattern) |
|||
|
|||
def callback_trackMetaDataChanged(self, exportDict, force=False): |
|||
""" |
|||
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. |
|||
""" |
|||
|
|||
if force or self.parentView.parentMainWindow.currentTrackId == exportDict["id"]: |
|||
self.trackName.setText(exportDict["name"]) |
|||
self.trackName.show() |
|||
c = QtGui.QColor(exportDict["color"]) |
|||
self.currentColor = c |
|||
if c.lightness() > 127: #between 0 (for black) and 255 (for white) |
|||
labelColor = QtGui.QColor("black") |
|||
else: |
|||
labelColor = QtGui.QColor("white") |
|||
self.labelColor = labelColor #save for new events |
|||
|
|||
for step in self._steps.values(): |
|||
if step.status: |
|||
step.setBrush(c) |
|||
step.velocityNumber.setBrush(labelColor) |
|||
|
|||
|
|||
def guicallback_subdivisionsChanged(self, newValue): |
|||
"""handle measuresPerGroup""" |
|||
#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.setApperance() |
|||
groupCounter, beatNumber = divmod(x, newValue) |
|||
if not beatNumber: |
|||
label = QtWidgets.QGraphicsSimpleTextItem(str(groupCounter+1)) |
|||
self.addItem(label) |
|||
label.setBrush(self.textColor) |
|||
x = x * SIZE_UNIT |
|||
x += SIZE_RIGHT_OFFSET |
|||
label.setPos(x+3, SIZE_TOP_OFFSET-13) |
|||
self._labels.append(label) |
|||
|
|||
def showVelocities(self): |
|||
for patternStep in self._steps.values(): |
|||
if patternStep.status: |
|||
patternStep.velocityNumber.show() |
|||
def hideVelocities(self): |
|||
for patternStep in self._steps.values(): |
|||
patternStep.velocityNumber.hide() |
|||
|
|||
def mousePressEvent(self, event): |
|||
self._middleMouseDown = False |
|||
if event.button() == QtCore.Qt.MiddleButton: |
|||
self._middleMouseDown = True |
|||
self._lastRow = None |
|||
self._play(event) |
|||
event.accept() |
|||
|
|||
if not type(self.itemAt(event.scenePos().x(), event.scenePos().y(), self.parentView.transform())) is Step: |
|||
self.showVelocities() |
|||
else: |
|||
event.ignore() |
|||
super().mousePressEvent(event) |
|||
|
|||
def _off(self): |
|||
if not self._lastRow is None: |
|||
api.noteOff(self.parentView.parentMainWindow.currentTrackId, self._lastRow) |
|||
self._lastRow = None |
|||
|
|||
def _play(self, event): |
|||
assert self._middleMouseDown |
|||
if not self.parentView.parentMainWindow.currentTrackId: |
|||
return |
|||
row = (event.scenePos().y() - SIZE_TOP_OFFSET) / SIZE_UNIT |
|||
if row >= 0: |
|||
row = int(row) |
|||
else: |
|||
row = -1 |
|||
|
|||
x = event.scenePos().x() |
|||
inside = x > SIZE_RIGHT_OFFSET and x < self.sceneRect().width() - SIZE_RIGHT_OFFSET |
|||
|
|||
if ( row < 0 or row > 7 ) or not inside : |
|||
row = None |
|||
|
|||
if not row == self._lastRow: |
|||
if not self._lastRow is None: |
|||
api.noteOff(self.parentView.parentMainWindow.currentTrackId, self._lastRow) |
|||
|
|||
if not row is None: |
|||
api.noteOn(self.parentView.parentMainWindow.currentTrackId, row) |
|||
|
|||
self._lastRow = row |
|||
|
|||
def mouseMoveEvent(self, event): |
|||
"""Event button is always 0 in a mouse move event""" |
|||
if self._middleMouseDown: |
|||
event.accept() |
|||
self._play(event) |
|||
else: |
|||
#Not for us, trigger, let other items decide. |
|||
event.ignore() |
|||
super().mouseMoveEvent(event) |
|||
|
|||
def mouseReleaseEvent(self, event): |
|||
if self._middleMouseDown: |
|||
self._off() |
|||
self._middleMouseDown = False |
|||
if event.button() == QtCore.Qt.MiddleButton: |
|||
event.accept() |
|||
self._lastRow = None |
|||
self.hideVelocities() |
|||
else: |
|||
event.ignore() |
|||
super().mousePressEvent(event) |
|||
|
|||
|
|||
def contextMenuEvent(self, event): |
|||
menu = QtWidgets.QMenu() |
|||
|
|||
trackId = self.parentView.parentMainWindow.currentTrackId |
|||
|
|||
listOfLabelsAndFunctions = [ |
|||
(QtCore.QCoreApplication.translate("EventContextMenu", "Invert Steps"), lambda: api.patternInvertSteps(trackId)), |
|||
(QtCore.QCoreApplication.translate("EventContextMenu", "All Steps On"), lambda: api.patternOnAllSteps(trackId)), |
|||
(QtCore.QCoreApplication.translate("EventContextMenu", "All Steps Off"), lambda: api.patternOffAllSteps(trackId)), |
|||
] |
|||
|
|||
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) |
|||
|
|||
pos = QtGui.QCursor.pos() |
|||
pos.setY(pos.y() + 5) |
|||
menu.exec_(pos) |
|||
|
|||
def wheelEvent(self, event): |
|||
"""zoom, otherwise ignore event""" |
|||
if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ControlModifier: |
|||
if event.delta() > 0: #zoom in |
|||
self._zoomFactor = min(5, round(self._zoomFactor + 0.25, 2)) |
|||
else: #zoom out |
|||
self._zoomFactor = max(0.1, round(self._zoomFactor - 0.25, 2)) |
|||
self._zoom(event) |
|||
event.accept() |
|||
else: |
|||
event.ignore() |
|||
super().wheelEvent(event) |
|||
|
|||
def _zoom(self, event): |
|||
if 0.1 < self._zoomFactor < 5: |
|||
self.parentView.resetTransform() |
|||
self.parentView.scale(self._zoomFactor, self._zoomFactor) |
|||
self.parentView.centerOn(event.scenePos()) |
|||
|
|||
|
|||
def createShadow(self, exportDict): |
|||
"""Receives steps from another track and display them as shadoy steps in the current one |
|||
as a reference. Creating a new shadow does not delete the old one. |
|||
""" |
|||
for x, y in ((s["index"], s["pitch"]) for s in exportDict["pattern"]): |
|||
self._steps[(x,y)].shadow = True # (x,y):Step() |
|||
self._steps[(x,y)].setApperance() |
|||
|
|||
def removeShadows(self): |
|||
for step in self._steps.values(): |
|||
if step.shadow: |
|||
step.shadow = False |
|||
step.setApperance() |
|||
|
|||
class Step(QtWidgets.QGraphicsRectItem): |
|||
|
|||
"""The representation of a note""" |
|||
def __init__(self, parentScene, column, row): #Factor and Velocity are set on activation |
|||
self.parentScene = parentScene |
|||
self.column = column #grid coordinates, not pixels |
|||
self.row = row |
|||
offset = 2 |
|||
self.offset = offset |
|||
self.defaultSize = (offset, offset, SIZE_UNIT-offset*2, SIZE_UNIT-offset*2) #x, y, w, h |
|||
super().__init__(*self.defaultSize) |
|||
self.setAcceptHoverEvents(True) |
|||
self.setFlags(QtWidgets.QGraphicsItem.ItemIsFocusable) #to receive key press events |
|||
self.main = True |
|||
self.exceedsPlayback = False |
|||
self.factor = api.DEFAULT_FACTOR |
|||
self.status = False |
|||
self._factorChangeAllowed = False #during drag and drop this will be True. Used in the mouse steps. |
|||
self.shadow = False |
|||
#Velocity |
|||
self._rememberVelocity = None |
|||
self.velocityNumber = QtWidgets.QGraphicsSimpleTextItem() |
|||
self.velocityNumber.setParentItem(self) |
|||
self.velocityNumber.setBrush(self.parentScene.labelColor) |
|||
self.velocityNumber.setPos(offset*2,offset*2) #that is not pretty but you can see it under the cursor |
|||
self.velocityNumber.hide() #only visible during mouse wheel event |
|||
|
|||
#The data section. On creation all the steps are uninitialized. They are off and hold no musical values |
|||
#self.velocity = set in self.useDefaultValues . We don't set it at all here to have a clear Exception when the program tries to access it. |
|||
#self.factor = set in self.useDefaultValues . We don't set it at all here to have a clear Exception when the program tries to access it. |
|||
#self.pitch #this is determined by the position on the grid |
|||
|
|||
self.setApperance() #sets color, size and exceedPlayback warning. not velocity. |
|||
|
|||
def setApperance(self): |
|||
"""sets color, main/sub size and exceedPlayback warning. not velocity. |
|||
This gets called quite often. On mouse down and on release for starters.""" |
|||
|
|||
def setWidth(): |
|||
if not self.exceedsPlayback and self.x() + self.rect().width() + SIZE_RIGHT_OFFSET> self.parentScene.sceneRect().right(): |
|||
self.exceedsPlayback = True |
|||
if self.exceedsPlayback: |
|||
rect = self.rect() |
|||
maximumWidth = self.parentScene.sceneRect().right() - self.x() - SIZE_RIGHT_OFFSET - self.offset*2 |
|||
rect.setWidth(maximumWidth) |
|||
self.setRect(rect) |
|||
else: |
|||
rect = self.rect() |
|||
rect.setWidth(SIZE_UNIT * self.factor - self.offset*2) |
|||
self.setRect(rect) |
|||
|
|||
if self.status: |
|||
setWidth() |
|||
assert self.parentScene.currentColor |
|||
self.setBrush(self.parentScene.currentColor) |
|||
self.velocityNumber.setBrush(self.parentScene.labelColor) |
|||
self.setZValue(_zValuesRelativeToScene["step"]) |
|||
else: |
|||
self.setOpacity(1) |
|||
if self.shadow: |
|||
setWidth() |
|||
color = self.parentScene.parentView.parentMainWindow.fPalBlue.color(QtGui.QPalette.Shadow) #this is already an existing instance |
|||
self.setOpacity(0.3) |
|||
self.setZValue(_zValuesRelativeToScene["shadow"]) |
|||
elif self.main: |
|||
color = self.parentScene.parentView.parentMainWindow.fPalBlue.color(QtGui.QPalette.AlternateBase) #this is already an existing instance |
|||
self.setZValue(_zValuesRelativeToScene["off"]) |
|||
else: |
|||
color = self.parentScene.parentView.parentMainWindow.fPalBlue.color(QtGui.QPalette.Base) #this is already an existing instance |
|||
self.setZValue(_zValuesRelativeToScene["off"]) |
|||
self.setBrush(color) |
|||
|
|||
@property |
|||
def velocity(self): |
|||
return self._velocity |
|||
|
|||
@velocity.setter |
|||
def velocity(self, value): |
|||
self._velocity = value |
|||
self.velocityNumber.setText(str(value)) |
|||
self.setOpacity(self._compress(value, 1, 127, 0.4, 1.0)) |
|||
|
|||
def _compress(self, input, inputLowest, inputHighest, outputLowest, outputHighest): |
|||
return (input-inputLowest) / (inputHighest-inputLowest) * (outputHighest-outputLowest) + outputLowest |
|||
|
|||
def export(self): |
|||
"""Make a dict to send to the engine""" |
|||
return { |
|||
"index":self.column, |
|||
"pitch":self.row, |
|||
"factor":self.factor, |
|||
"velocity":self.velocity} |
|||
|
|||
def useDefaultValues(self): |
|||
self.velocity = api.getAverageVelocity(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId) #already sets opacity and velocityNumber |
|||
self._rememberVelocity = self.velocity |
|||
self.factor = api.DEFAULT_FACTOR |
|||
self.initalized = True |
|||
|
|||
def on(self, velocityAndFactor=None, exceedsPlayback=None): |
|||
"""velocityAndFactor is a tuple""" |
|||
if velocityAndFactor: #on load / by callback |
|||
self.velocity, self.factor = velocityAndFactor |
|||
else: #User clicked on an empty field. |
|||
self.useDefaultValues() |
|||
|
|||
self.exceedsPlayback = exceedsPlayback |
|||
|
|||
assert self.factor > 0 |
|||
rect = self.rect() |
|||
#rect.setWidth(self.defaultSize[2] * self.factor) |
|||
rect.setWidth(SIZE_UNIT * self.factor - self.offset*2) |
|||
self.setRect(rect) |
|||
|
|||
self.status = True |
|||
self.setApperance() #sets color, main/sub size and exceedPlayback warning |
|||
|
|||
def off(self): |
|||
self.status = False |
|||
self.setRect(*self.defaultSize) |
|||
self.setApperance() #sets color, main/sub size and exceedPlayback warning |
|||
self.velocityNumber.hide() #just in case. |
|||
|
|||
|
|||
def mousePressEvent(self, event): |
|||
if event.button() == QtCore.Qt.LeftButton: |
|||
event.accept() |
|||
if self.status: |
|||
self.off() |
|||
api.removeStep(self.parentScene.parentView.parentMainWindow.currentTrackId, self.column, self.row) |
|||
else: |
|||
self.on() |
|||
self._factorChangeAllowed = True |
|||
self._factorStartTime = time() #see mouseReleaseEvent |
|||
else: |
|||
event.ignore() |
|||
|
|||
def mouseMoveEvent(self, event): |
|||
if self._factorChangeAllowed: |
|||
# < is left to right |
|||
# > is right to left |
|||
event.accept() |
|||
rect = self.rect() |
|||
if event.lastScenePos().x() < event.scenePos().x(): |
|||
new = event.scenePos().x() - self.x() |
|||
else: |
|||
new = max(self.defaultSize[2]/2, event.scenePos().x() - self.x()) #pixel values, not tick, nor factor |
|||
rect.setRight(new) |
|||
self.setRect(rect) |
|||
|
|||
def mouseReleaseEvent(self, event): |
|||
if self._factorChangeAllowed: |
|||
assert self.status |
|||
self._factorChangeAllowed = False |
|||
width = self.rect().width() + self.offset*2 |
|||
value = width / SIZE_UNIT |
|||
elapsedTime = time() - self._factorStartTime #to prevent hectic mouse pressing from triggering the factor we only accept a change if a certain time treshold was passed |
|||
if (elapsedTime > 0.2 and value >= 0.5):# or value == 0.5: |
|||
self.factor = value |
|||
self.setApperance() #sets color, size and exceedPlayback warning |
|||
else: # A quick mouseclick |
|||
assert self.factor == 1 |
|||
self.setRect(*self.defaultSize) #we reset this in case something goes wrong. If everything is all right we will a receive a callback to set the width anyway, before the user sees anything. |
|||
self.setApperance() #sets color, size and exceedPlayback warning |
|||
api.setStep(self.parentScene.parentView.parentMainWindow.currentTrackId, self.export()) |
|||
event.accept() |
|||
|
|||
def hoverEnterEvent(self, event): |
|||
"""Hover events are only triggered if no mouse button is down. The mouse got grabbed |
|||
by the item. Which makes sense because we want to receive mouseRelease on an item even |
|||
if the mouse cursor is not on that item anymore""" |
|||
if self.status: |
|||
event.accept() |
|||
self._rememberVelocity = self.velocity |
|||
else: |
|||
event.ignore() |
|||
|
|||
def hoverLeaveEvent(self, event): |
|||
"""Hover events are only triggered if no mouse button is down. The mouse got grabbed |
|||
by the item. Which makes sense because we want to receive mouseRelease on an item even |
|||
if the mouse cursor is not on that item anymore""" |
|||
self.velocityNumber.hide() |
|||
if self.status: |
|||
event.accept() |
|||
if self.status and not self.velocity == self._rememberVelocity: |
|||
api.setStep(self.parentScene.parentView.parentMainWindow.currentTrackId, self.export()) |
|||
self._rememberVelocity = self.velocity |
|||
else: |
|||
event.ignore() |
|||
|
|||
|
|||
def wheelEvent(self, event): |
|||
"""This buffers until hoverLeaveEvent and then the new value is sent in self.hoverLeaveEvent""" |
|||
if self.status: |
|||
event.accept() |
|||
self.velocityNumber.show() |
|||
if event.delta() > 0: |
|||
self.velocity += 2 |
|||
if self.velocity >= 127: |
|||
self.velocity = 127 |
|||
else: |
|||
self.velocity -= 2 |
|||
if self.velocity <= 2: |
|||
self.velocity = 1 |
|||
else: |
|||
event.ignore() |
|||
|
|||
|
|||
class Scale(QtWidgets.QGraphicsRectItem): |
|||
def __init__(self, parentScene): |
|||
super().__init__(0,0,0,0) |
|||
self.parentScene = parentScene |
|||
self.pitchWidgets = [] #sorted from top to bottom in Step Rect and scene coordinates |
|||
self.noteNames = [] #list of 128 notes. use index with note name. Can be changed at runtime. |
|||
api.callbacks.trackMetaDataChanged.append(self.callback_trackMetaDataChanged) |
|||
self.buildScale() #also sets the positions of the buttons above |
|||
|
|||
def callback_trackMetaDataChanged(self, exportDict): |
|||
#Order matters. We need to set the notenames before the scale. |
|||
self.setNoteNames(exportDict["noteNames"]) |
|||
self.setScale(exportDict["scale"]) |
|||
|
|||
def buildScale(self): |
|||
"""Only executed once per pattern""" |
|||
for i in range(api.NUMBER_OF_STEPS): |
|||
p = PitchWidget(parentItem=self) |
|||
y = i * SIZE_UNIT |
|||
p.setParentItem(self) |
|||
p.setPos(-65, y+10) |
|||
self.pitchWidgets.append(p) |
|||
#self.setRect(0,0, SIZE_RIGHT_OFFSET, p.y() + SIZE_UNIT) #p is the last of the 8. |
|||
|
|||
def setScale(self, scaleList): |
|||
"""We receive from top to bottom, in step rect coordinates. This is not sorted after |
|||
pitches! Pitches can be any order the user wants. |
|||
""" |
|||
for widget, scaleMidiPitch in zip(self.pitchWidgets, scaleList): |
|||
widget.spinBox.setValue(scaleMidiPitch) |
|||
widget.rememberLastValue = scaleMidiPitch |
|||
|
|||
def setNoteNames(self, pNoteNames): |
|||
"""A list of 128 strings. Gets only called by the callback.""" |
|||
#if pNoteNames in pitch.notenames.keys(): |
|||
# self.noteNames = pitch.notenames[pNoteNames] |
|||
#else: |
|||
self.noteNames = pNoteNames |
|||
for pitchWidget in self.pitchWidgets: |
|||
pitchWidget.spinBoxValueChanged() #change all current pitchWidgets |
|||
|
|||
def sendToEngine(self): |
|||
result = [widget.spinBox.value() for widget in self.pitchWidgets] |
|||
#result.reverse() |
|||
api.setScale(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId, scale=result) |
|||
|
|||
class TransposeControls(QtWidgets.QWidget): |
|||
"""Communication with the scale spinBoxes is done via api callbacks. We just fire and forget""" |
|||
|
|||
#Not working. the translation generate works statically. translatedScales = [QtCore.QT_TRANSLATE_NOOP("Scale", scale) for scale in api.schemes] |
|||
#No choice but to prepare the translations manually here. At least we do not need to watch for the order. |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "Major") |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "Minor") |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "Dorian") |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "Phrygian") |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "Lydian") |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "Mixolydian") |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "Locrian") |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "Blues") |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "Hollywood") |
|||
|
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "English") |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "Lilypond") |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "German") |
|||
QtCore.QT_TRANSLATE_NOOP("Scale", "Drums GM") |
|||
|
|||
|
|||
def __init__(self, parentScene): |
|||
self.parentScene = parentScene |
|||
super().__init__() |
|||
|
|||
layout = QtWidgets.QHBoxLayout() |
|||
layout.setSpacing(0) |
|||
layout.setContentsMargins(0,0,0,0) |
|||
self.setLayout(layout) |
|||
|
|||
transposeUp = QtWidgets.QPushButton(QtCore.QCoreApplication.translate("TransposeControls", "+Half Tone")) |
|||
transposeUp.clicked.connect(self.transposeUp) |
|||
transposeUp.setToolTip(QtCore.QCoreApplication.translate("TransposeControls", "Transpose the whole scale up a half tone (+1 midi note)")) |
|||
layout.addWidget(transposeUp) |
|||
|
|||
transposeDown = QtWidgets.QPushButton(QtCore.QCoreApplication.translate("TransposeControls", "-Half Tone")) |
|||
transposeDown.clicked.connect(self.transposeDown) |
|||
transposeDown.setToolTip(QtCore.QCoreApplication.translate("TransposeControls", "Transpose the whole scale down a half tone (-1 midi note)")) |
|||
layout.addWidget(transposeDown) |
|||
|
|||
transposeUpOctave = QtWidgets.QPushButton(QtCore.QCoreApplication.translate("TransposeControls", "+Octave")) |
|||
transposeUpOctave.clicked.connect(self.transposeUpOctave) |
|||
transposeUpOctave.setToolTip(QtCore.QCoreApplication.translate("TransposeControls", "Transpose the whole scale up an octave (+12 midi notes)")) |
|||
layout.addWidget(transposeUpOctave) |
|||
|
|||
transposeDownOctave = QtWidgets.QPushButton(QtCore.QCoreApplication.translate("TransposeControls", "-Octave")) |
|||
transposeDownOctave.clicked.connect(self.transposeDownOctave) |
|||
transposeDownOctave.setToolTip(QtCore.QCoreApplication.translate("TransposeControls", "Transpose the whole scale down an octave (-12 midi notes)")) |
|||
layout.addWidget(transposeDownOctave) |
|||
|
|||
translatedSchemes = [QtCore.QCoreApplication.translate("Scale", scheme) for scheme in api.schemes] |
|||
|
|||
transposeToScale = QtWidgets.QComboBox() |
|||
self._transposeToScaleWidget = transposeToScale |
|||
transposeToScale.addItems([QtCore.QCoreApplication.translate("TransposeControls","Set Scale to:")] + translatedSchemes) #This is a hack. QProxyWidgets will draw outside of the view and cannot be seen anymore. We reset to the 0th entry after each change. |
|||
transposeToScale.activated.connect(self.transposeToScale) #activated, not changend. even when choosing the same item |
|||
transposeToScale.setToolTip(QtCore.QCoreApplication.translate("TransposeControls", "Take the bottom note and build a predefined scale from it upwards.")) |
|||
layout.addWidget(transposeToScale) |
|||
|
|||
|
|||
self._comboBoxNoteNames = QtWidgets.QComboBox() |
|||
translatedNotenames = [QtCore.QCoreApplication.translate("Scale", scheme) for scheme in sorted(list(pitch.noteNames.keys()))] |
|||
self._comboBoxNoteNames.addItems([QtCore.QCoreApplication.translate("TransposeControls","Set Notenames to:")] + translatedNotenames) |
|||
self._comboBoxNoteNames.activated.connect(self._changeNoteNamesByDropdown) #activated, not changend. even when choosing the same item |
|||
self._comboBoxNoteNames.setToolTip(QtCore.QCoreApplication.translate("TransposeControls", "Use this scheme as note names.")) |
|||
layout.addWidget(self._comboBoxNoteNames) |
|||
|
|||
def _changeNoteNamesByDropdown(self, index): |
|||
if index > 0: |
|||
index -= 1 # we inserted our "Set NoteNames to" at index 0 shifting the real values +1. |
|||
|
|||
schemes = sorted(pitch.noteNames.keys()) |
|||
noteNamesAsString = sorted(pitch.noteNames.keys())[index] |
|||
noteNames = pitch.noteNames[noteNamesAsString] |
|||
api.setNoteNames(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId, noteNames=noteNames) |
|||
self._comboBoxNoteNames.blockSignals(True) |
|||
self._comboBoxNoteNames.setCurrentIndex(0) |
|||
self._comboBoxNoteNames.blockSignals(False) |
|||
|
|||
def transposeUp(self): |
|||
api.transposeHalftoneSteps(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId, steps=1) |
|||
|
|||
def transposeDown(self): |
|||
api.transposeHalftoneSteps(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId, steps=-1) |
|||
|
|||
|
|||
def transposeUpOctave(self): |
|||
api.transposeHalftoneSteps(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId, steps=12) |
|||
|
|||
def transposeDownOctave(self): |
|||
api.transposeHalftoneSteps(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId, steps=-12) |
|||
|
|||
def transposeToScale(self, index): |
|||
if index > 0: |
|||
index -= 1 # the backend list obviously has no "Set Scale to" on index [0] |
|||
api.setScaleToKeyword(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId, keyword=api.schemes[index]) #this schemes must NOT be translated since it is the original key/symbol. |
|||
self._transposeToScaleWidget.blockSignals(True) |
|||
self._transposeToScaleWidget.setCurrentIndex(0) |
|||
self._transposeToScaleWidget.blockSignals(False) |
|||
|
|||
class PitchWidget(QtWidgets.QGraphicsProxyWidget): |
|||
""" A PitchWidget has a variable width by nature because the note-name can vary. |
|||
For that reason We need to truncate to match the fixed size. |
|||
Offset and position are set in Scale.buildScale |
|||
""" |
|||
|
|||
def __init__(self, parentItem): |
|||
super().__init__() |
|||
self.parentItem = parentItem |
|||
self.spinBox = QtWidgets.QSpinBox() |
|||
#self.spinBox.setFrame(True) |
|||
self.spinBox.setMinimum(0) |
|||
self.spinBox.setMaximum(127) |
|||
self.spinBox.stepBy = self.stepBy |
|||
#self.spinBox.setValue(0) #No init value. This is changed on active track callback |
|||
|
|||
widget = QtWidgets.QWidget() |
|||
layout = QtWidgets.QHBoxLayout() |
|||
layout.setSpacing(0) |
|||
layout.setContentsMargins(0,0,0,0) |
|||
widget.setLayout(layout) |
|||
widget.setStyleSheet(".QWidget { background-color: rgba(0,0,0,0) }") #transparent, but only this widget, hence the leading dot |
|||
|
|||
self.label = QtWidgets.QLabel() #changed in spinBoxValueChanged |
|||
self.label.setText("") |
|||
self.label.setFixedSize(90, 18) |
|||
self.label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter) |
|||
|
|||
layout.addWidget(self.label) |
|||
layout.addWidget(self.spinBox) |
|||
|
|||
self.setWidget(widget) |
|||
|
|||
self.spinBox.valueChanged.connect(self.spinBoxValueChanged) |
|||
self.spinBox.editingFinished.connect(self.spinBoxEditingFinished) |
|||
#self.spinBoxValueChanged() #Delay that. The engine Data is not ready yet. It will be sent by the callback |
|||
|
|||
arrowsLeftStylesheet = """ |
|||
QSpinBox { |
|||
padding-left: 15px; /* make room for the arrows */ |
|||
} |
|||
|
|||
QSpinBox::up-button { |
|||
subcontrol-position: top left; /* position at the top right corner */ |
|||
} |
|||
|
|||
QSpinBox::down-button { |
|||
subcontrol-position: bottom left; /* position at bottom right corner */ |
|||
} |
|||
""" |
|||
#That does not looks good |
|||
#self.spinBox.setStyleSheet(arrowsLeftStylesheet) |
|||
|
|||
def midiToNotename(self, midipitch): |
|||
assert self.parentItem.noteNames, self.parentItem.noteNames |
|||
try: |
|||
return self.parentItem.noteNames[midipitch] #includes octave names |
|||
except IndexError: |
|||
print (midipitch) |
|||
print (self.parentItem.noteNames) |
|||
exit() |
|||
|
|||
def spinBoxValueChanged(self): |
|||
self.label.setText(self.midiToNotename(self.spinBox.value())) |
|||
|
|||
def spinBoxEditingFinished(self): |
|||
if not self.rememberLastValue == self.spinBox.value(): |
|||
self.parentItem.sendToEngine() |
|||
self.rememberLastValue = self.spinBox.value() |
|||
|
|||
def stepBy(self, n): |
|||
"""Override standard behaviour to make page up and page down go in octaves, not in 10""" |
|||
if n == 10: |
|||
QtWidgets.QAbstractSpinBox.stepBy(self.spinBox, 12) |
|||
elif n == -10: |
|||
QtWidgets.QAbstractSpinBox.stepBy(self.spinBox, -12) |
|||
else: |
|||
QtWidgets.QAbstractSpinBox.stepBy(self.spinBox, n) |
|||
|
|||
|
|||
class Playhead(QtWidgets.QGraphicsLineItem): |
|||
def __init__(self, parentScene): |
|||
super().__init__(0, 0, 0, api.NUMBER_OF_STEPS*SIZE_UNIT) # (x1, y1, x2, y2) |
|||
self.parentScene = parentScene |
|||
p = QtGui.QPen() |
|||
p.setColor(QtGui.QColor("red")) |
|||
p.setWidth(3) |
|||
#p.setCosmetic(True) |
|||
self.setPen(p) |
|||
api.callbacks.setPlaybackTicks.append(self.setCursorPosition) |
|||
self.setZValue(90) |
|||
|
|||
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 |
|||
x += SIZE_RIGHT_OFFSET |
|||
if playbackStatus: # api.duringPlayback: |
|||
self.show() |
|||
self.setX(x) |
|||
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) |
|||
else: |
|||
self.hide() |
|||
|
|||
|
|||
class VelocityControls(QtWidgets.QWidget): |
|||
def __init__(self, parentScene): |
|||
self.parentScene = parentScene |
|||
super().__init__() |
|||
|
|||
layout = QtWidgets.QHBoxLayout() |
|||
layout.setSpacing(0) |
|||
layout.setContentsMargins(0,0,0,0) |
|||
self.setLayout(layout) |
|||
|
|||
velocityUp = QtWidgets.QPushButton(QtCore.QCoreApplication.translate("VelocityControls", "+Velocity")) |
|||
velocityUp.clicked.connect(self.velocityUp) |
|||
velocityUp.wheelEvent = self._mouseWheelEvent |
|||
velocityUp.setToolTip(QtCore.QCoreApplication.translate("VelocityControls", "Make everything louder. Hover and mousewheel up/down to go in steps of 10.")) |
|||
layout.addWidget(velocityUp) |
|||
|
|||
velocityDown = QtWidgets.QPushButton(QtCore.QCoreApplication.translate("VelocityControls", "-Velocity")) |
|||
velocityDown.clicked.connect(self.velocityDown) |
|||
velocityDown.wheelEvent = self._mouseWheelEvent |
|||
velocityDown.setToolTip(QtCore.QCoreApplication.translate("VelocityControls", "Make everything softer. Hover and mousewheel up/down to go in steps of 10.")) |
|||
layout.addWidget(velocityDown) |
|||
|
|||
def _mouseWheelEvent(self, event): |
|||
event.accept() |
|||
if event.angleDelta().y() > 0: #up |
|||
api.changePatternVelocity(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId, steps=10) |
|||
else: #down |
|||
api.changePatternVelocity(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId, steps=-10) |
|||
#The steps got redrawn, therefore their velocity numbers are now invislbe again. We are still in hover modus: |
|||
self.parentScene.showVelocities() |
|||
|
|||
def enterEvent(self, event): |
|||
self.parentScene.showVelocities() |
|||
|
|||
def leaveEvent(self, event): |
|||
self.parentScene.hideVelocities() |
|||
|
|||
def velocityUp(self): |
|||
api.changePatternVelocity(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId, steps=1) |
|||
#The steps got redrawn, therefore their velocity numbers are now invislbe again. We are still in hover modus: |
|||
self.parentScene.showVelocities() |
|||
|
|||
def velocityDown(self): |
|||
api.changePatternVelocity(trackId=self.parentScene.parentView.parentMainWindow.currentTrackId, steps=-1) |
|||
#The steps got redrawn, therefore their velocity numbers are now invislbe again. We are still in hover modus: |
|||
self.parentScene.showVelocities() |
|||
|
|||
|
|||
|
|||
|
@ -0,0 +1,793 @@ |
|||
#! /usr/bin/env python3 |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
Copyright 2018, Nils Hilbricht, Germany ( https://www.hilbricht.net ) |
|||
|
|||
This file is part of Patroneo ( https://www.laborejo.org ) |
|||
|
|||
Laborejo is free software: you can redistribute it and/or modify |
|||
it under the terms of the GNU General Public License as published by |
|||
the Free Software Foundation, either version 3 of the License, or |
|||
(at your option) any later version. |
|||
|
|||
This program is distributed in the hope that it will be useful, |
|||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
GNU General Public License for more details. |
|||
|
|||
You should have received a copy of the GNU General Public License |
|||
along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
""" |
|||
|
|||
from time import time |
|||
import engine.api as api #Session is already loaded and created, no duplication. |
|||
from PyQt5 import QtCore, QtGui, QtWidgets |
|||
|
|||
SIZE_UNIT = 25 #this is in manual sync with timeline.py SIZE_UNIT |
|||
SIZE_TOP_OFFSET = 0 |
|||
|
|||
_zValuesRelativeToScene = { #Only use for objects added directly to the scene, not children. |
|||
"trackStructure":3, |
|||
"switch":4, |
|||
"barline":5, |
|||
"playhead":90, |
|||
} |
|||
|
|||
class SongEditor(QtWidgets.QGraphicsScene): |
|||
def __init__(self, parentView): |
|||
super().__init__() |
|||
self.parentView = parentView |
|||
|
|||
#Subitems |
|||
self.playhead = Playhead(parentScene = self) |
|||
self.addItem(self.playhead) |
|||
self.playhead.setY(SIZE_TOP_OFFSET) |
|||
|
|||
self.tracks = {} #TrackID:TrackStructures |
|||
self.barlines = [] #in order |
|||
self.cachedCombinedTrackHeight = 0 #set by callback_tracksChanged |
|||
self.trackOrder = [] #set by callback_numberOfTracksChanged |
|||
|
|||
role = QtGui.QPalette.BrightText |
|||
self.brightPen = QtGui.QPen(self.parentView.parentMainWindow.fPalBlue.color(role)) |
|||
self.normalPen = QtGui.QPen() |
|||
|
|||
api.callbacks.numberOfTracksChanged.append(self.callback_numberOfTracksChanged) |
|||
api.callbacks.timeSignatureChanged.append(self.callback_timeSignatureChanged) |
|||
api.callbacks.numberOfMeasuresChanged.append(self.callback_setnumberOfMeasures) |
|||
|
|||
api.callbacks.trackStructureChanged.append(self.callback_trackStructureChanged) #updates single tracks |
|||
api.callbacks.exportCacheChanged.append(self.cacheExportDict) |
|||
api.callbacks.scoreChanged.append(self.callback_scoreChanged) #sends information about measuresPerGroup |
|||
api.callbacks.trackMetaDataChanged.append(self.callback_trackMetaDataChanged) |
|||
|
|||
#self.ticksToPixelRatio = None set by callback_timeSignatureChanged |
|||
|
|||
def wheelEvent(self, event): |
|||
"""zoom, otherwise ignore event""" |
|||
if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ControlModifier: |
|||
self.parentView.parentMainWindow.zoomUpperHalf(event.delta()) |
|||
event.accept() |
|||
else: |
|||
event.ignore() |
|||
super().wheelEvent(event) |
|||
|
|||
def callback_trackMetaDataChanged(self, exportDict): |
|||
"""This is not for the initial track creation, only for later changes""" |
|||
self.tracks[exportDict["id"]].updateMetaData(exportDict) |
|||
|
|||
def cacheExportDict(self, exportDict): |
|||
"""Does not get called on structure change because callback_trackStructureChanged |
|||
also caches the exportDict """ |
|||
self.tracks[exportDict["id"]].exportDict = exportDict |
|||
|
|||
def callback_trackStructureChanged(self, exportDict): |
|||
"""Happens if a switch gets flipped""" |
|||
track = self.tracks[exportDict["id"]] |
|||
track.updateSwitches(exportDict) |
|||
|
|||
def callback_timeSignatureChanged(self, nr, typ): |
|||
oneMeasureInTicks = nr * typ |
|||
self.ticksToPixelRatio = oneMeasureInTicks / SIZE_UNIT |
|||
|
|||
def callback_numberOfTracksChanged(self, exportDictList): |
|||
"""Used for new tracks, delete track and move track""" |
|||
toDelete = set(self.tracks.keys()) |
|||
self.trackOrder = [] |
|||
|
|||
for index, exportDict in enumerate(exportDictList): |
|||
if exportDict["id"] in self.tracks: |
|||
toDelete.remove(exportDict["id"]) #keep this track and don't delete later. |
|||
else: #new track |
|||
self.tracks[exportDict["id"]] = TrackStructure(parentScene=self) |
|||
self.addItem(self.tracks[exportDict["id"]]) |
|||
self.tracks[exportDict["id"]].setZValue(_zValuesRelativeToScene["trackStructure"]) |
|||
|
|||
self.trackOrder.append(self.tracks[exportDict["id"]]) |
|||
self.tracks[exportDict["id"]].setY(index * SIZE_UNIT + SIZE_TOP_OFFSET) |
|||
self.tracks[exportDict["id"]].updateSwitches(exportDict) |
|||
self.tracks[exportDict["id"]].updateStaffLines(exportDict["numberOfMeasures"]) |
|||
|
|||
#We had these tracks in the GUI but they are gone in the export. This is track delete. |
|||
for trackId in toDelete: |
|||
trackStructure = self.tracks[trackId] |
|||
#we don't need to delete from trackOrder here because that is cleared each time we call this function |
|||
del self.tracks[trackId] |
|||
self.removeItem(trackStructure) #remove from scene |
|||
del trackStructure |
|||
|
|||
assert all(track.exportDict["index"] == self.trackOrder.index(track) for track in self.tracks.values()) |
|||
self.cachedCombinedTrackHeight = len(self.tracks) * SIZE_UNIT |
|||
self.setSceneRect(0,0,exportDict["numberOfMeasures"]*SIZE_UNIT,self.cachedCombinedTrackHeight + 3*SIZE_TOP_OFFSET) #no empty space on top without a scene rect. Also a bit of leniance. |
|||
self.playhead.setLine(0, 0, 0, self.cachedCombinedTrackHeight) #(x1, y1, x2, y2) |
|||
self.adjustBarlineHeightForNewTrackCount() |
|||
|
|||
def adjustBarlineHeightForNewTrackCount(self): |
|||
"""Fetches the current context itself and modifies all existing barlines. |
|||
""" |
|||
for barline in self.barlines: |
|||
barline.setLine(0,0,0,self.cachedCombinedTrackHeight) |
|||
|
|||
def callback_setnumberOfMeasures(self, exportDictScore): |
|||
requestAmountOfMeasures = exportDictScore["numberOfMeasures"] |
|||
requestAmountOfMeasures += 1 #the final closing barline |
|||
maximumAmountIncludingHidden = len(self.barlines) |
|||
|
|||
if requestAmountOfMeasures == maximumAmountIncludingHidden: |
|||
for l in self.barlines: l.show() |
|||
elif requestAmountOfMeasures > maximumAmountIncludingHidden: #we need more than we have. Maybe new ones. |
|||
for l in self.barlines: l.show() |
|||
for i in range(maximumAmountIncludingHidden, requestAmountOfMeasures): |
|||
barline = QtWidgets.QGraphicsLineItem(0,0,0,1) #correct length will be set below, but we need something other than 0 here |
|||
self.addItem(barline) |
|||
barline.setAcceptedMouseButtons(QtCore.Qt.NoButton) #barlines will intercept clicks on the track otherwise. We keep the horizontal stafflines blocking to prevent accidents though. |
|||
barline.setPos(i*SIZE_UNIT, SIZE_TOP_OFFSET) |
|||
barline.setEnabled(False) |
|||
barline.setZValue(_zValuesRelativeToScene["barline"]) |
|||
self.barlines.append(barline) |
|||
else: #user reduced the number of barlines. We only hide, never delete. |
|||
for l in self.barlines[requestAmountOfMeasures:]: |
|||
l.hide() |
|||
|
|||
#Guaranteed visible. |
|||
for l in self.barlines[:requestAmountOfMeasures]: |
|||
l.show() |
|||
|
|||
self.callback_scoreChanged(exportDictScore) #colors from the start |
|||
|
|||
self.adjustBarlineHeightForNewTrackCount() #otherwise only the new ones have the correct height. |
|||
|
|||
self.setSceneRect(0,0,requestAmountOfMeasures*SIZE_UNIT,self.cachedCombinedTrackHeight + 3*SIZE_TOP_OFFSET) #no empty space on top without a scene rect |
|||
|
|||
for track in self.tracks.values(): |
|||
track.updateSwitchVisibility(requestAmountOfMeasures=requestAmountOfMeasures-1) |
|||
track.updateStaffLines(requestAmountOfMeasures-1) |
|||
|
|||
def callback_scoreChanged(self, exportDictScore): |
|||
self.measuresPerGroupCache = exportDictScore["measuresPerGroup"] |
|||
for i,barline in enumerate(self.barlines): |
|||
if i > 0 and (i+1) % exportDictScore["measuresPerGroup"] == 1: |
|||
barline.setPen(self.brightPen) |
|||
else: |
|||
barline.setPen(self.normalPen) |
|||
|
|||
class TrackStructure(QtWidgets.QGraphicsRectItem): |
|||
"""From left to right. Holds two lines to show the "stafflinen" and a number of switches, |
|||
colored rectangles to indicate where a pattern is activated on the timeline""" |
|||
|
|||
def __init__(self, parentScene): |
|||
super().__init__(0,0,1,SIZE_UNIT) |
|||
self.parentScene = parentScene |
|||
|
|||
self.exportDict = None #self.update gets called immediately after creation. |
|||
self.switches = {} # position:switchInstance |
|||
|
|||
self.currentColor = None #set in updateMetaData |
|||
self.labelColor = None #set in updateMetaData for redable labels on our color. for example transpose number |
|||
#The track holds the horizontal lines. The number of barlines is calculated in the parentScene for all tracks at once. |
|||
self.topLine = QtWidgets.QGraphicsLineItem(0,0,1,0) #empty line is not possible. We need at least something to let self.update work with |
|||
self.topLine.setParentItem(self) |
|||
self.topLine.setPos(0,0) |
|||
self.bottomLine = QtWidgets.QGraphicsLineItem(0,0,1,0) #empty line is not possible. We need at least something to let self.update work with |
|||
self.bottomLine.setParentItem(self) |
|||
self.bottomLine.setPos(0,SIZE_UNIT) |
|||
|
|||
self.topLine.setEnabled(False) |
|||
self.bottomLine.setEnabled(False) |
|||
|
|||
#Interactive Marker to select several switches in a row |
|||
self._mousePressOn = None #to remember the position of a mouse click |
|||
#self._markerLine = QtWidgets.QGraphicsLineItem(0,0,10,0) #only updated, never replaced |
|||
self._markerLine = QtWidgets.QGraphicsRectItem(0,0,10,SIZE_UNIT) #only updated, never replaced |
|||
self._markerLine.setParentItem(self) |
|||
self._markerLine.setY(0) #x is set in mousePressEvent |
|||
self._markerLine.setZValue(_zValuesRelativeToScene["playhead"]) |
|||
self._markerLine.hide() |
|||
|
|||
def _setColors(self, exportDict): |
|||
c = QtGui.QColor(exportDict["color"]) |
|||
self.currentColor = c |
|||
|
|||
if c.lightness() > 127: #between 0 (for black) and 255 (for white) |
|||
labelColor = QtGui.QColor("black") |
|||
else: |
|||
labelColor = QtGui.QColor("white") |
|||
self.labelColor = labelColor #save for new switches |
|||
|
|||
|
|||
def updateSwitches(self, exportDict): |
|||
self.exportDict = exportDict |
|||
self._setColors(exportDict) |
|||
|
|||
#Create new switches |
|||
for position in exportDict["structure"]: |
|||
if not position in self.switches: |
|||
self.switches[position] = self._createSwitch(position) |
|||
|
|||
self.updateSwitchVisibility(exportDict["numberOfMeasures"]) |
|||
|
|||
def updateMetaData(self, exportDict): |
|||
"""Color and Transposition status. |
|||
Does not get called on track structure change.""" |
|||
self._setColors(exportDict) |
|||
|
|||
for switch in self.switches.values(): |
|||
switch.setBrush(self.currentColor) |
|||
switch.setScaleTransposeColor(self.labelColor) |
|||
switch.setHalftoneTransposeColor(self.labelColor) |
|||
|
|||
def updateStaffLines(self, requestAmountOfMeasures): |
|||
l = self.topLine.line() |
|||
l.setLength(requestAmountOfMeasures * SIZE_UNIT) |
|||
self.topLine.setLine(l) |
|||
|
|||
l = self.bottomLine.line() |
|||
l.setLength(requestAmountOfMeasures * SIZE_UNIT) |
|||
self.bottomLine.setLine(l) |
|||
|
|||
#Update self, which is the track background |
|||
self.setRect(0,0,requestAmountOfMeasures * SIZE_UNIT, SIZE_UNIT) |
|||
|
|||
def _createSwitch(self, position): |
|||
"""Called only by self.updateSwitches |
|||
Qt can't put the same item into the scene twice. We need to create a new one each time""" |
|||
switch = Switch(parentTrackStructure=self, position=position) |
|||
assert self.currentColor |
|||
switch.setBrush(self.currentColor) |
|||
switch.setParentItem(self) |
|||
switch.setX(position * SIZE_UNIT) |
|||
return switch |
|||
|
|||
def updateSwitchVisibility(self, requestAmountOfMeasures): |
|||
"""Switch pattern-visibility on and off. |
|||
This never creates or deletes switches |
|||
We assume self.exportDict is up to date |
|||
because we get called by self.updateSwitches, which saves the exportDict.""" |
|||
structure = self.exportDict["structure"] |
|||
whichPatternsAreScaleTransposed = self.exportDict["whichPatternsAreScaleTransposed"] |
|||
whichPatternsAreHalftoneTransposed = self.exportDict["whichPatternsAreHalftoneTransposed"] |
|||
for position, switch in self.switches.items(): |
|||
if position < requestAmountOfMeasures and position in structure: |
|||
switch.show() |
|||
else: |
|||
switch.hide() #Not delete because this may be just a temporary reduction of measures |
|||
switch.scaleTransposeOff() |
|||
|
|||
if position in whichPatternsAreScaleTransposed: |
|||
switch.setScaleTranspose(-1 * whichPatternsAreScaleTransposed[position]) #we flip the polarity from "makes sense" to row based "lower is higher" here. The opposite, sending, flip is done in switch hover leave event |
|||
else: |
|||
switch.scaleTransposeOff() |
|||
|
|||
if position in whichPatternsAreHalftoneTransposed: |
|||
switch.setHalftoneTranspose(whichPatternsAreHalftoneTransposed[position]) #half tone transposition is not flipped |
|||
else: |
|||
switch.halftoneTransposeOff() |
|||
|
|||
def scenePos2switchPosition(self, x): |
|||
return int(x / SIZE_UNIT) |
|||
|
|||
def mousePressEvent(self, event): |
|||
#First we need to find the mouse clicks position. self.switches only holds pos that were at least activated once. |
|||
#The track is only the area where the rectangles and lines meet. it is impossible to click below or right of the tracks. |
|||
#we always get a valid position this way. |
|||
|
|||
if event.button() == QtCore.Qt.MiddleButton and not self._mousePressOn: |
|||
self.parentScene.parentView.parentMainWindow.patternGrid.createShadow(self.exportDict) |
|||
else: |
|||
self.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.exportDict) |
|||
if event.button() == QtCore.Qt.LeftButton: |
|||
assert not self._mousePressOn |
|||
position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based |
|||
self._markerLine.setX(position * SIZE_UNIT ) |
|||
|
|||
newBool = not position in self.switches or not self.switches[position].isVisible() |
|||
if newBool: |
|||
self._markerLine.setBrush(self.currentColor) |
|||
else: |
|||
self._markerLine.setBrush(self.parentScene.parentView.parentMainWindow.fPalBlue.color(QtGui.QPalette.AlternateBase)) #we are always the active track so this is our color |
|||
|
|||
self._mousePressOn = (time(), self, position, newBool) #Reset to None in mouseReleaseEvent |
|||
result = api.setSwitch(self.exportDict["id"], position, newBool) #returns True if a switch happend |
|||
assert result |
|||
|
|||
#elif event.button() == QtCore.Qt.RightButton and not self._mousePressOn: |
|||
#no, this is done with contextMenuEvent directly so it also reacts to the context menu keyboard key. |
|||
|
|||
def contextMenuEvent(self, event): |
|||
if self._mousePressOn: #Right click can happen while the left button is still pressed down, which we don't want. |
|||
return |
|||
menu = QtWidgets.QMenu() |
|||
|
|||
position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based |
|||
measuresPerGroup = self.parentScene.measuresPerGroupCache |
|||
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)), |
|||
] |
|||
|
|||
for text, function in listOfLabelsAndFunctions: |
|||
a = QtWidgets.QAction(text, menu) |
|||
menu.addAction(a) |
|||
a.triggered.connect(function) |
|||
|
|||
pos = QtGui.QCursor.pos() |
|||
pos.setY(pos.y() + 5) |
|||
self.parentScene.parentView.parentMainWindow.setFocus() |
|||
menu.exec_(pos) |
|||
|
|||
def mouseMoveEvent(self, event): |
|||
position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based |
|||
if self._mousePressOn and position != self._mousePressOn[2]: |
|||
#self._markerLine.setLine(0,0, (position - self._mousePressOn[2])*SIZE_UNIT + SIZE_UNIT/2, 0) |
|||
rect = self._markerLine.rect() |
|||
if position < 0: |
|||
position = 0 |
|||
elif position + 1 > self.exportDict["numberOfMeasures"]: #position is already a switch position |
|||
position = self.exportDict["numberOfMeasures"] - 1 |
|||
|
|||
if position < self._mousePressOn[2]: |
|||
left = (position - self._mousePressOn[2]) * SIZE_UNIT |
|||
rect.setLeft(left) |
|||
rect.setRight(SIZE_UNIT) |
|||
else: |
|||
right = (position - self._mousePressOn[2]) * SIZE_UNIT + SIZE_UNIT |
|||
rect.setRight(right) |
|||
rect.setLeft(0) |
|||
|
|||
self._markerLine.setRect(rect) |
|||
self._markerLine.show() |
|||
else: |
|||
self._markerLine.hide() |
|||
|
|||
|
|||
def mouseReleaseEvent(self, event): |
|||
if event.button() == QtCore.Qt.LeftButton: |
|||
self._markerLine.hide() |
|||
position = self.scenePos2switchPosition(event.scenePos().x()) |
|||
if position < 0: |
|||
position = 0 |
|||
elif position +1 > self.exportDict["numberOfMeasures"]: #position is already a switch position |
|||
position = self.exportDict["numberOfMeasures"] -1 |
|||
|
|||
startTime, startTrack, startPosition, setTo = self._mousePressOn |
|||
self._mousePressOn = None |
|||
if not startPosition == position and time() - startTime > 0.4: #optimisation to spare the engine from redundant work. Also prevent hectic drag-clicking |
|||
#setTo is a bool that tells us if all the switches in our range should go on (True) or off (False). The first switch, startPosition, is already set in mousePressEvent for a better user experience. |
|||
low, high = sorted((startPosition, position)) #both included |
|||
setOfPositions = set(range(low, high+1)) #range does not include the last one, we want it in. it MUST be a set. |
|||
api.setSwitches(self.exportDict["id"], setOfPositions, setTo) |
|||
|
|||
def mark(self, boolean): |
|||
"""Mark the whole Track as active or not""" |
|||
if boolean: |
|||
role = QtGui.QPalette.AlternateBase |
|||
else: |
|||
role = QtGui.QPalette.Base |
|||
c = self.parentScene.parentView.parentMainWindow.fPalBlue.color(role) |
|||
self.setBrush(c) |
|||
|
|||
class Switch(QtWidgets.QGraphicsRectItem): |
|||
"""Switches live for the duration of the track. Once created they only ever get hidden/shown, |
|||
never deleted.""" |
|||
def __init__(self, parentTrackStructure, position): |
|||
self.parentTrackStructure = parentTrackStructure |
|||
self.position = position |
|||
super().__init__(0,0,SIZE_UNIT,SIZE_UNIT) |
|||
|
|||
self.setAcceptHoverEvents(True) |
|||
|
|||
self.setZValue(_zValuesRelativeToScene["switch"]) |
|||
|
|||
self.scaleTransposeGlyph = QtWidgets.QGraphicsSimpleTextItem("") |
|||
self.scaleTransposeGlyph.setParentItem(self) |
|||
self.scaleTransposeGlyph.setScale(0.80) |
|||
self.scaleTransposeGlyph.setPos(2,1) |
|||
self.scaleTransposeGlyph.setBrush(self.parentTrackStructure.labelColor) |
|||
self.scaleTransposeGlyph.hide() |
|||
self.scaleTranspose = 0 |
|||
|
|||
self.halftoneTransposeGlyph = QtWidgets.QGraphicsSimpleTextItem("") |
|||
self.halftoneTransposeGlyph.setParentItem(self) |
|||
self.halftoneTransposeGlyph.setScale(0.80) |
|||
self.halftoneTransposeGlyph.setPos(1,13) |
|||
self.halftoneTransposeGlyph.setBrush(self.parentTrackStructure.labelColor) |
|||
self.halftoneTransposeGlyph.hide() |
|||
self.halftoneTranspose = 0 |
|||
|
|||
def setScaleTranspose(self, value): |
|||
""" |
|||
Called by track callbacks and also for the temporary buffer display |
|||
|
|||
while internally both the engine and us, the GUI, use steps and transposition through |
|||
"negative is higher pitch" we present it reversed for the user. |
|||
Greater number is higher pitch |
|||
|
|||
It is guaranteed that only active switches can have a transposition. |
|||
Also transposition=0 is not included. |
|||
""" |
|||
self.scaleTranspose = value |
|||
self._setScaleTransposeLabel(value) |
|||
|
|||
def _setScaleTransposeLabel(self, value): |
|||
text = ("+" if value > 0 else "") + str(value) + "s" |
|||
self.scaleTransposeGlyph.setText(text) |
|||
self.scaleTransposeGlyph.show() |
|||
|
|||
def setScaleTransposeColor(self, c): |
|||
self.scaleTransposeGlyph.setBrush(c) |
|||
|
|||
def scaleTransposeOff(self): |
|||
self.scaleTransposeGlyph.setText("") |
|||
#self.scaleTransposeGlyph.hide() |
|||
self.scaleTranspose = 0 |
|||
self._bufferScaleTranspose = 0 |
|||
|
|||
def setHalftoneTranspose(self, value): |
|||
self.halftoneTranspose = value |
|||
self._setHalftoneTransposeLabel(value) |
|||
|
|||
def _setHalftoneTransposeLabel(self, value): |
|||
text = ("+" if value > 0 else "") + str(value) + "h" |
|||
self.halftoneTransposeGlyph.setText(text) |
|||
self.halftoneTransposeGlyph.show() |
|||
|
|||
def setHalftoneTransposeColor(self, c): |
|||
self.halftoneTransposeGlyph.setBrush(c) |
|||
|
|||
def halftoneTransposeOff(self): |
|||
self.halftoneTransposeGlyph.setText("") |
|||
#self.halftoneTransposeGlyph.hide() |
|||
self.halftoneTranspose = 0 |
|||
self._bufferhalftoneTranspose = 0 |
|||
|
|||
def mousePressEvent(self, event): |
|||
"""A mouse events on the track activate a switch. Then we receive the event to turn it |
|||
off again.""" |
|||
event.ignore() |
|||
|
|||
def hoverEnterEvent(self, event): |
|||
self._bufferScaleTranspose = self.scaleTranspose |
|||
self._bufferHalftoneTranspose = self.halftoneTranspose |
|||
|
|||
def hoverLeaveEvent(self, event): |
|||
"""only triggered when active/shown""" |
|||
event.accept() |
|||
|
|||
#Scale Transpose. Independent of Halftone Transpose |
|||
if not self._bufferScaleTranspose == self.scaleTranspose: |
|||
api.setSwitchScaleTranspose(self.parentTrackStructure.exportDict["id"], self.position, -1*self._bufferScaleTranspose) #we flip the polarity here. The receiving flip is done in the callback. |
|||
#new transpose/buffer gets set via callback |
|||
if self._bufferScaleTranspose == 0: |
|||
self.scaleTransposeOff() |
|||
|
|||
#Halftone Transpose. Independent of Scale Transpose |
|||
if not self._bufferHalftoneTranspose == self.halftoneTranspose: |
|||
api.setSwitchHalftoneTranspose(self.parentTrackStructure.exportDict["id"], self.position, self._bufferHalftoneTranspose) #half tone transposition is not flipped |
|||
#new transpose/buffer gets set via callback |
|||
if self._bufferHalftoneTranspose == 0: |
|||
self.halftoneTransposeOff() |
|||
|
|||
def wheelEvent(self, event): |
|||
"""Does not get triggered when switch is off. |
|||
This buffers until hoverLeaveEvent and then the new value is sent in self.hoverLeaveEvent""" |
|||
event.accept() |
|||
|
|||
if QtWidgets.QApplication.keyboardModifiers() == QtCore.Qt.ShiftModifier: #half tone transposition |
|||
if event.delta() > 0: |
|||
self._bufferHalftoneTranspose = min(+24, self._bufferHalftoneTranspose+1) |
|||
else: |
|||
self._bufferHalftoneTranspose = max(-24, self._bufferHalftoneTranspose-1) |
|||
self._setHalftoneTransposeLabel(self._bufferHalftoneTranspose) |
|||
|
|||
else: #scale transposition |
|||
if event.delta() > 0: |
|||
self._bufferScaleTranspose = min(+7, self._bufferScaleTranspose+1) |
|||
else: |
|||
self._bufferScaleTranspose = max(-7, self._bufferScaleTranspose-1) |
|||
self._setScaleTransposeLabel(self._bufferScaleTranspose) |
|||
|
|||
|
|||
class TrackLabelEditor(QtWidgets.QGraphicsScene): |
|||
"""Only the track labels""" |
|||
def __init__(self, parentView): |
|||
super().__init__() |
|||
self.parentView = parentView |
|||
self.tracks = {} #TrackID:TrackStructures |
|||
|
|||
api.callbacks.numberOfTracksChanged.append(self.callback_numberOfTracksChanged) |
|||
api.callbacks.trackMetaDataChanged.append(self.callback_trackMetaDataChanged) |
|||
api.callbacks.exportCacheChanged.append(self.cacheExportDict) |
|||
self.cachedCombinedTrackHeight = 0 #set by callback_tracksChanged |
|||
|
|||
def cacheExportDict(self, exportDict): |
|||
self.tracks[exportDict["id"]].exportDict = exportDict |
|||
|
|||
def callback_trackMetaDataChanged(self, exportDict): |
|||
"""This is not for the initial track creation, only for later changes""" |
|||
self.tracks[exportDict["id"]].update(exportDict) |
|||
|
|||
def callback_numberOfTracksChanged(self, exportDictList): |
|||
toDelete = set(self.tracks.keys()) |
|||
|
|||
width = self.parentView.geometry().width() |
|||
|
|||
for index, exportDict in enumerate(exportDictList): |
|||
if exportDict["id"] in self.tracks: |
|||
toDelete.remove(exportDict["id"]) |
|||
else: #new track |
|||
self.tracks[exportDict["id"]] = TrackLabel(parentScene=self, width=width, height=SIZE_UNIT) |
|||
self.addItem(self.tracks[exportDict["id"]]) |
|||
|
|||
self.tracks[exportDict["id"]].setY(index * SIZE_UNIT + SIZE_TOP_OFFSET) |
|||
self.tracks[exportDict["id"]].update(exportDict) |
|||
|
|||
#We had this tracks in the GUI but they are gone in the export. This is track delete. |
|||
for trackId in toDelete: |
|||
trackLabel = self.tracks[trackId] |
|||
del self.tracks[trackId] |
|||
self.removeItem(trackLabel) #remove from scene |
|||
del trackLabel |
|||
if toDelete and self.parentView.parentMainWindow.currentTrackId in toDelete: #toDelete still exist if tracks were deleted above |
|||
anyExistingTrack = next(iter(self.tracks.values())) |
|||
self.parentView.parentMainWindow.chooseCurrentTrack(anyExistingTrack.exportDict) |
|||
|
|||
self.cachedCombinedTrackHeight = len(self.tracks) * SIZE_UNIT |
|||
self.setSceneRect(0,0,width-1,self.cachedCombinedTrackHeight + 3*SIZE_TOP_OFFSET) |
|||
|
|||
def contextMenuEvent(self, event): |
|||
""" |
|||
We can't delete this properly object from within. The engine callback will react faster |
|||
than we need to finish this function. That means qt and python will try to access |
|||
objects that are non-existent""" |
|||
|
|||
menu = QtWidgets.QMenu() |
|||
|
|||
item = self.itemAt(event.scenePos().x(), event.scenePos().y(), self.parentView.transform()) |
|||
if not type(item) is QtWidgets.QGraphicsProxyWidget: |
|||
return None |
|||
|
|||
exportDict = item.parentItem().exportDict.copy() |
|||
del item #yes, manual memory management in Python. We need to get rid of anything we want to delete later or Qt will crash because we have a python wrapper without a qt object |
|||
|
|||
listOfLabelsAndFunctions = [ |
|||
(exportDict["name"], None), |
|||
(QtCore.QCoreApplication.translate("TrackLabelContext", "Invert Measures"), lambda: api.trackInvertSwitches(exportDict["id"])), |
|||
(QtCore.QCoreApplication.translate("TrackLabelContext", "All Measures On"), lambda: api.trackOnAllSwitches(exportDict["id"])), |
|||
(QtCore.QCoreApplication.translate("TrackLabelContext", "All Measures Off"), lambda: api.trackOffAllSwitches(exportDict["id"])), |
|||
(QtCore.QCoreApplication.translate("TrackLabelContext", "Clone this Track"), lambda: api.createSiblingTrack(exportDict["id"])), |
|||
(QtCore.QCoreApplication.translate("TrackLabelContext", "Delete Track"), lambda: api.deleteTrack(exportDict["id"])), |
|||
] |
|||
|
|||
for text, function in listOfLabelsAndFunctions: |
|||
if function is None: |
|||
l = QtWidgets.QLabel(text) |
|||
l.setAlignment(QtCore.Qt.AlignCenter) |
|||
a = QtWidgets.QWidgetAction(menu) |
|||
a.setDefaultWidget(l) |
|||
menu.addAction(a) |
|||
else: |
|||
a = QtWidgets.QAction(text, menu) |
|||
menu.addAction(a) |
|||
a.triggered.connect(function) |
|||
|
|||
#Add a submenu for merge/copy |
|||
mergeMenu = menu.addMenu(QtCore.QCoreApplication.translate("TrackLabelContext", "Merge/Copy from")) |
|||
|
|||
def createCopyMergeLambda(srcId): |
|||
return lambda: api.trackMergeCopyFrom(srcId, exportDict["id"]) |
|||
|
|||
for track in self.tracks.values(): |
|||
sourceDict = track.exportDict |
|||
a = QtWidgets.QAction(sourceDict["name"], mergeMenu) |
|||
mergeMenu.addAction(a) |
|||
mergeCommand = createCopyMergeLambda(sourceDict["id"]) |
|||
if sourceDict["id"] == exportDict["id"]: |
|||
a.setEnabled(False) |
|||
a.triggered.connect(mergeCommand) |
|||
|
|||
pos = QtGui.QCursor.pos() |
|||
pos.setY(pos.y() + 5) |
|||
self.parentView.parentMainWindow.setFocus() |
|||
menu.exec_(pos) |
|||
|
|||
class TrackLabel(QtWidgets.QGraphicsRectItem): |
|||
def __init__(self, parentScene, width, height): |
|||
super().__init__(0, 0, width, height) |
|||
self.parentScene = parentScene |
|||
self.setPen(QtGui.QPen(QtCore.Qt.NoPen)) |
|||
|
|||
self.positioningHandle = TrackLabel.PositioningHandle(parentTrackLabel=self) |
|||
self.positioningHandle.setParentItem(self) |
|||
self.positioningHandle.setPos(0,0) |
|||
self.positioningHandle.setToolTip(QtCore.QCoreApplication.translate("TrackLabel", "grab and move to reorder tracks")) |
|||
|
|||
self.colorButton = TrackLabel.ColorPicker(parentTrackLabel=self) |
|||
self.colorButton.setParentItem(self) |
|||
self.colorButton.setPos(SIZE_UNIT, 3) |
|||
self.colorButton.setToolTip(QtCore.QCoreApplication.translate("TrackLabel", "change track color")) |
|||
|
|||
self.lineEdit = TrackLabel.NameLineEdit(parentTrackLabel=self) |
|||
self.label = QtWidgets.QGraphicsProxyWidget() |
|||
self.label.setWidget(self.lineEdit) |
|||
self.label.setParentItem(self) |
|||
self.label.setPos(2*SIZE_UNIT+3,0) |
|||
|
|||
self.setFlag(self.ItemIgnoresTransformations) |
|||
|
|||
class ColorPicker(QtWidgets.QGraphicsRectItem): |
|||
def __init__(self, parentTrackLabel): |
|||
super().__init__(0,0,SIZE_UNIT*0.75,SIZE_UNIT*0.75) |
|||
self.parentTrackLabel = parentTrackLabel |
|||
self.setBrush(QtGui.QColor("cyan")) |
|||
|
|||
def mousePressEvent(self, event): |
|||
if event.button() == QtCore.Qt.LeftButton: |
|||
self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict) |
|||
event.accept() |
|||
colorDialog = QtWidgets.QColorDialog() |
|||
color = colorDialog.getColor(self.brush().color()) #blocks |
|||
if color.isValid(): #and not abort |
|||
#self.setBrush(color) #done via callback. |
|||
api.changeTrackColor(self.parentTrackLabel.exportDict["id"], color.name()) |
|||
#else: |
|||
# colorDialog.setStandardColor(self.brush().color()) |
|||
else: |
|||
event.ignore() |
|||
#super().mousePressEvent(event) |
|||
|
|||
class PositioningHandle(QtWidgets.QGraphicsEllipseItem): |
|||
def __init__(self, parentTrackLabel): |
|||
super().__init__(0,0,SIZE_UNIT-2,SIZE_UNIT-2) |
|||
self.parentTrackLabel = parentTrackLabel |
|||
self.setPen(QtGui.QPen(QtCore.Qt.NoPen)) |
|||
role = QtGui.QPalette.ToolTipBase |
|||
c = self.parentTrackLabel.parentScene.parentView.parentMainWindow.fPalBlue.color(role) |
|||
self.setBrush(c) |
|||
self.setOpacity(0.08) #this is meant as a slight overlay/highlight of both the current track and the other tracks |
|||
|
|||
self.arrowLabel = QtWidgets.QGraphicsSimpleTextItem("↕") |
|||
self.arrowLabel.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresParentOpacity) |
|||
self.arrowLabel.setParentItem(self) |
|||
self.arrowLabel.setScale(1.6) |
|||
self.arrowLabel.setPos(2,1) |
|||
role = QtGui.QPalette.Text |
|||
self.arrowLabel.setBrush(self.parentTrackLabel.parentScene.parentView.parentMainWindow.fPalBlue.color(role)) |
|||
|
|||
self._cursorPosOnMoveStart = None |
|||
|
|||
def yPos2trackIndex(self, y): |
|||
"""0 based""" |
|||
pos = round(y / SIZE_UNIT) |
|||
pos = min(pos, len(self.parentTrackLabel.parentScene.tracks)-1) |
|||
return pos |
|||
|
|||
def mouseMoveEvent(self, event): |
|||
if self._cursorPosOnMoveStart: |
|||
self.parentTrackLabel.setY(max(0, event.scenePos().y())) |
|||
#super().mouseMoveEvent(event) #with this the sync between cursor and item is off. |
|||
|
|||
def mousePressEvent(self, event): |
|||
"""release gets only triggered when mousePressEvent was on the same item. |
|||
We don't need to worry about the user just releasing the mouse on this item""" |
|||
self._posBeforeMove = self.parentTrackLabel.pos() |
|||
self._cursorPosOnMoveStart = QtGui.QCursor.pos() |
|||
|
|||
self._lineCursor = self.parentTrackLabel.lineEdit.cursor() |
|||
self.parentTrackLabel.mousePressEvent(event) |
|||
#super().mousePressEvent(event) #with this in mouseMoveEvent does not work. IIRC because we do not set the movableFlag |
|||
|
|||
def mouseReleaseEvent(self, event): |
|||
newIndex = self.yPos2trackIndex(self.parentTrackLabel.y()) #we need to save that first, right after this we reset the position |
|||
self.parentTrackLabel.setPos(self._posBeforeMove) #In case the block was moved to a position where no track is (below the tracks) we just reset the graphics before anything happens. The user will never see this really |
|||
self._posBeforeMove = None |
|||
self._cursorPosOnMoveStart = None |
|||
api.moveTrack(self.parentTrackLabel.exportDict["id"], newIndex) |
|||
|
|||
class NameLineEdit(QtWidgets.QLineEdit): |
|||
def __init__(self, parentTrackLabel): |
|||
super().__init__("") |
|||
self.parentTrackLabel = parentTrackLabel |
|||
self.setFrame(False) |
|||
self.setMaxLength(25) |
|||
self.setMinimumSize(QtCore.QSize(0, SIZE_UNIT)) |
|||
self.setStyleSheet("background-color: rgba(0,0,0,0)") #transparent so we see the RectItem color |
|||
self.setReadOnly(True) |
|||
self.setFocusPolicy(QtCore.Qt.ClickFocus) #nmo tab |
|||
self.editingFinished.connect(self.sendToEngine) |
|||
self.returnPressed.connect(self.enter) |
|||
|
|||
def mousePressEvent(self,event): |
|||
"""We also need to force this track as active""" |
|||
event.accept() #we need this for doubleClick |
|||
self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict) |
|||
#event.ignore() #send to parent instead |
|||
#super().mousePressEvent(event) |
|||
|
|||
def mouseDoubleClickEvent(self, event): |
|||
event.accept() |
|||
self.setReadOnly(False) |
|||
|
|||
def enter(self): |
|||
self.sendToEngine() |
|||
|
|||
def sendToEngine(self): |
|||
self.setReadOnly(True) |
|||
new = self.text() |
|||
if not new == self.parentTrackLabel.exportDict["name"]: |
|||
self.blockSignals(True) |
|||
api.changeTrackName(self.parentTrackLabel.exportDict["id"], new) |
|||
self.blockSignals(False) |
|||
|
|||
#def keyPressEvent(self, event): |
|||
# if event.key()) == QtCore.Qt.Key_Return: |
|||
# event.accept() |
|||
# |
|||
# else: |
|||
# event.ignore() |
|||
# super().keyPressEvent(event) |
|||
|
|||
|
|||
def update(self, exportDict): |
|||
self.lineEdit.setText(exportDict["name"]) |
|||
self.exportDict = exportDict |
|||
self.colorButton.setBrush(QtGui.QColor(exportDict["color"])) |
|||
|
|||
def mousePressEvent(self,event): |
|||
self.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.exportDict) |
|||
#event.ignore() #don't add that here. Will destroy mouseMove and Release events of child widgets |
|||
|
|||
#def mouseReleaseEvent(self, event): |
|||
# event. |
|||
|
|||
def mark(self, boolean): |
|||
if boolean: |
|||
role = QtGui.QPalette.AlternateBase |
|||
else: |
|||
role = QtGui.QPalette.Base |
|||
c = self.parentScene.parentView.parentMainWindow.fPalBlue.color(role) |
|||
self.setBrush(c) |
|||
|
|||
class Playhead(QtWidgets.QGraphicsLineItem): |
|||
def __init__(self, parentScene): |
|||
super().__init__(0, 0, 0, 0) # (x1, y1, x2, y2) |
|||
self.parentScene = parentScene |
|||
p = QtGui.QPen() |
|||
p.setColor(QtGui.QColor("red")) |
|||
p.setWidth(3) |
|||
#p.setCosmetic(True) |
|||
self.setPen(p) |
|||
api.callbacks.setPlaybackTicks.append(self.setCursorPosition) |
|||
self.setZValue(_zValuesRelativeToScene["playhead"]) |
|||
|
|||
def setCursorPosition(self, tickindex, playbackStatus): |
|||
"""Set the playhead to the right position, but keep the viewport stable. |
|||
Shift the entire "page" if the cursor becomes invisible because its steps outside the viewport""" |
|||
x = tickindex / self.parentScene.ticksToPixelRatio |
|||
self.setX(x) |
|||
if playbackStatus: # api.duringPlayback: |
|||
scenePos = self.parentScene.parentView.mapFromScene(self.pos()) |
|||
cursorViewPosX = scenePos.x() #the cursor position in View coordinates |
|||
width = self.parentScene.parentView.geometry().width() |
|||
if cursorViewPosX <= 0 or cursorViewPosX >= width: #"pageflip" |
|||
self.parentScene.parentView.horizontalScrollBar().setValue(x) |
@ -0,0 +1,138 @@ |
|||
#! /usr/bin/env python3 |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
Copyright 2018, 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/>. |
|||
""" |
|||
|
|||
|
|||
SIZE_UNIT = 25 #this is in manual sync with songeditor.py SIZE_UNIT |
|||
|
|||
import engine.api as api #Session is already loaded and created, no duplication. |
|||
from PyQt5 import QtCore, QtGui, QtWidgets |
|||
|
|||
class Timeline(QtWidgets.QGraphicsScene): |
|||
def __init__(self, parentView): |
|||
super().__init__() |
|||
self.parentView = parentView |
|||
self.addItem(TimelineRect(parentScene=self)) |
|||
|
|||
|
|||
class TimelineRect(QtWidgets.QGraphicsRectItem): |
|||
"""Shows information about song progression. |
|||
JACK transport only shares the current time. |
|||
We cannot draw anything ahead of time other than what we know ourselves. |
|||
We rely on external tempo information and cannot react to tempo changes. |
|||
Our linear value is measures, so we display these.""" |
|||
def __init__(self, parentScene): |
|||
self.height = 25 |
|||
super().__init__(0, 0, 1, self.height) |
|||
self.parentScene = parentScene |
|||
self._cachedExportDictScore = {} |
|||
|
|||
role = QtGui.QPalette.Light |
|||
c = self.parentScene.parentView.parentMainWindow.fPalBlue.color(role) |
|||
self.setBrush(c) |
|||
|
|||
role = QtGui.QPalette.BrightText |
|||
self.brightText = self.parentScene.parentView.parentMainWindow.fPalBlue.color(role) |
|||
|
|||
self._cachedSubdivisions = 1 |
|||
self.measureNumbers = [] |
|||
#self._buffer_measuresPerGroup set in callback_setnumberOfMeasures, changed in wheelEvent. Utilized in hoverLeaveEvent |
|||
|
|||
self._pressed = False |
|||
|
|||
api.callbacks.numberOfMeasuresChanged.append(self.callback_setnumberOfMeasures) |
|||
api.callbacks.scoreChanged.append(self.callback_setnumberOfMeasures) #sends information about measuresPerGroup |
|||
api.callbacks.subdivisionsChanged.append(self.cache_subdivisions) #sends information about measuresPerGroup |
|||
api.callbacks.timeSignatureChanged.append(self.cache_timesignature) |
|||
|
|||
self.setToolTip(QtCore.QCoreApplication.translate("Timeline", "Click to set playback position. Scroll with mousewheel to adjust measure grouping.")) |
|||
|
|||
def cache_timesignature(self, howManyUnits, whatTypeOfUnit): |
|||
self._cachedExportDictScore["howManyUnits"] = howManyUnits |
|||
self._cachedExportDictScore["whatTypeOfUnit"] = whatTypeOfUnit |
|||
|
|||
def cache_subdivisions(self, subdivisions): |
|||
self._cachedSubdivisions = subdivisions |
|||
|
|||
def callback_setnumberOfMeasures(self, exportDictScore): |
|||
"""We only draw one number and line for each group and not the barlines in between""" |
|||
self._cachedExportDictScore = exportDictScore |
|||
requestAmountOfMeasures = exportDictScore["numberOfMeasures"] |
|||
self._buffer_measuresPerGroup = exportDictScore["measuresPerGroup"] |
|||
|
|||
self.setRect(0,0,requestAmountOfMeasures * SIZE_UNIT, SIZE_UNIT) |
|||
|
|||
#Delete old |
|||
for l in self.measureNumbers: |
|||
l.setParentItem(None) |
|||
self.parentScene.removeItem(l) |
|||
self.measureNumbers = [] |
|||
|
|||
#Create new |
|||
for i in range(requestAmountOfMeasures+1): |
|||
if i > 0 and (i+1) % exportDictScore["measuresPerGroup"] == 1: |
|||
measure = QtWidgets.QGraphicsSimpleTextItem(str(i)) #str(i).zfill(3) |
|||
measure.setBrush(self.brightText) |
|||
measure.setParentItem(self) |
|||
measure.setPos((i-1)*SIZE_UNIT, 5) #some magic pixel values for finetuning. |
|||
#measure.setEnabled(False) #Contrary to intuition this will not make this item ignore mouse clicks but just eat them. Enabling fowards mouse item to the timeline below. |
|||
measure.setFlag(self.ItemIgnoresTransformations) |
|||
self.measureNumbers.append(measure) |
|||
|
|||
barline = QtWidgets.QGraphicsLineItem(0,0,0,self.height) |
|||
barline.setParentItem(self) |
|||
barline.setPen(self.brightText) |
|||
barline.setPos(i*SIZE_UNIT, 0) |
|||
#barline.setEnabled(False) #Contrary to intuition this will not make this item ignore mouse clicks but just eat them. Enabling fowards mouse item to the timeline below. |
|||
barline.setFlag(self.ItemIgnoresTransformations) |
|||
self.measureNumbers.append(barline) |
|||
|
|||
|
|||
def _sendPlaybackPositionToEngine(self, posX): |
|||
oneMeasureInTicks = ( self._cachedExportDictScore["howManyUnits"] * self._cachedExportDictScore["whatTypeOfUnit"] ) / self._cachedSubdivisions |
|||
ratio = oneMeasureInTicks / SIZE_UNIT |
|||
value = posX * ratio |
|||
api.seek(int(value)) |
|||
|
|||
def mousePressEvent(self, event): |
|||
if event.button() == QtCore.Qt.LeftButton: |
|||
event.accept() |
|||
self._sendPlaybackPositionToEngine(event.scenePos().x()) |
|||
self._pressed = True |
|||
|
|||
def mouseMoveEvent(self, event): |
|||
if self._pressed: |
|||
self._sendPlaybackPositionToEngine(event.scenePos().x()) |
|||
event.accept() |
|||
|
|||
def mouseReleaseEvent(self, event): |
|||
if event.button() == QtCore.Qt.LeftButton: |
|||
event.accept() |
|||
self._pressed = False |
|||
|
|||
|
|||
def wheelEvent(self, event): |
|||
"""This buffers until hoverLeaveEvent and then the new value is sent in self.hoverLeaveEvent""" |
|||
event.accept() |
|||
if event.delta() > 0: |
|||
self._buffer_measuresPerGroup += 1 |
|||
else: |
|||
self._buffer_measuresPerGroup = max(1, self._buffer_measuresPerGroup-1) |
|||
api.set_measuresPerGroup(self._buffer_measuresPerGroup) |
Loading…
Reference in new issue