You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

470 lines
24 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
Laborejo2 is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Library
#Third party
from PyQt5 import QtCore, QtGui, QtWidgets
translate = QtCore.QCoreApplication.translate
#Template
from template.helper import pairwise
#Our own files
from .constantsAndConfigs import constantsAndConfigs
from .designer.trackWidget import Ui_trackGroupWidget
from .submenus import TickWidget, CombinedTickWidget
from contextlib import contextmanager
import engine.api as api
class TrackWidget(QtWidgets.QGroupBox):
#TODO: ideas: number of blocks, which CCs are set, review/change durationSettingsSignature, dynamicSettingsSignature
def __init__(self, parentDataEditor, trackExportObject):
super().__init__()
self.parentDataEditor = parentDataEditor
self.trackExportObject = trackExportObject
self.ui = Ui_trackGroupWidget()
self.ui.setupUi(self)
self.setAttribute(QtCore.Qt.WA_DeleteOnClose)
self.trackExportObject = trackExportObject #updated on every self.updateData
self.ui.visibleCheckbox.clicked.connect(self.visibleToggled) #only user changes, not through setChecked()
self.ui.audibleCheckbox.clicked.connect(self.audibleToggled) #only user changes, not through setChecked()
self.ui.doubleTrackCheckbox.clicked.connect(self.doubleTrackToggled) #only user changes, not through setChecked()
#self.ui.upbeatSpinBox.editingFinished.connect(self.upbeatChanged) #only user changes, not through setText() etc.
self.ui.upbeatSpinBox.valueChanged.connect(self.upbeatChanged) #also through the tickWidget
self.ui.callTickWidget.clicked.connect(self.callClickWidgetForUpbeat)
self.ui.nameLineEdit.editingFinished.connect(self.nameChanged) #only user changes, not through setText() etc.
self.ui.deleteButton.clicked.connect(lambda: api.deleteTrack(self.trackExportObject["id"]))
self.ui.midiChannelSpinBox.valueChanged.connect(self.dataChanged)
self.ui.midiProgramSpinBox.valueChanged.connect(self.dataChanged)
self.ui.midiBankMsbSpinBox.valueChanged.connect(self.dataChanged)
self.ui.midiBankLsbSpinBox.valueChanged.connect(self.dataChanged)
self.ui.midiTransposeSpinBox.valueChanged.connect(self.dataChanged)
self.ui.instrumentName.editingFinished.connect(self.nameChanged)
self.ui.shortInstrumentName.editingFinished.connect(self.nameChanged)
#Create a menu with checkboxes to allow switching on and off of additional channels for the CC sub-track
#However, we will not use normal checkable Menu actions since they close the menu after triggering. even blockSignals does not prevent closing
self.ccChannels = {}
ccChannelMenu = QtWidgets.QMenu()
for i in range(1,17): #excluding 17
checkbox = QtWidgets.QCheckBox(ccChannelMenu)
self.ccChannels[i] = checkbox
checkbox.stateChanged.connect(self.dataChanged)
widget = QtWidgets.QWidget()
layout = QtWidgets.QFormLayout()
layout.setContentsMargins(0,0,0,0) #left, top, right, bottom in pixel
widget.setLayout(layout)
widgetAction = QtWidgets.QWidgetAction(ccChannelMenu)
widgetAction.setDefaultWidget(widget)
layout.addRow(str(i).zfill(2), checkbox)
ccChannelMenu.addAction(widgetAction)
#action = QtWidgets.QAction(str(i), ccChannelMenu)
#action.setCheckable(True)
#ccChannelMenu.addAction(action)
self.ui.ccChannelsPushButton.setMenu(ccChannelMenu)
#TODO: The callbacks below trigger quite often. If you move the mouse wheel in a spin box each increase/decrease sends ALL track settings to the backend which triggers a compelte redraw of the track. This should be measure on performance and improved. But even incremental updates should have an immediate effect on a running playback so "send changes when closing the track editor" is only a compromise". Incremental updates are not a a solution either because even those need a playback update. valueChanged does not have an editingFinished signal like text fields.
#Set up the advanced properties. They should be hidden at the start
#Tab 1: Durations
self.ui.defaultOn.editingFinished.connect(self.dataChanged) #only user changes, not through setText() etc.
self.ui.defaultOff.editingFinished.connect(self.dataChanged)
self.ui.staccatoOn.editingFinished.connect(self.dataChanged)
self.ui.staccatoOff.editingFinished.connect(self.dataChanged)
self.ui.tenutoOn.editingFinished.connect(self.dataChanged)
self.ui.tenutoOff.editingFinished.connect(self.dataChanged)
self.ui.legatoOn.editingFinished.connect(self.dataChanged)
self.ui.legatoOff.editingFinished.connect(self.dataChanged)
#Tab 2: Dynamics
self.ui.dynamics_ppppp.valueChanged.connect(self.dataChanged)
self.ui.dynamics_pppp.valueChanged.connect(self.dataChanged)
self.ui.dynamics_ppp.valueChanged.connect(self.dataChanged)
self.ui.dynamics_pp.valueChanged.connect(self.dataChanged)
self.ui.dynamics_p.valueChanged.connect(self.dataChanged)
self.ui.dynamics_mp.valueChanged.connect(self.dataChanged)
self.ui.dynamics_mf.valueChanged.connect(self.dataChanged)
self.ui.dynamics_f.valueChanged.connect(self.dataChanged)
self.ui.dynamics_ff.valueChanged.connect(self.dataChanged)
self.ui.dynamics_fff.valueChanged.connect(self.dataChanged)
self.ui.dynamics_ffff.valueChanged.connect(self.dataChanged)
self.ui.dynamics_custom.valueChanged.connect(self.dataChanged)
self.ui.dynamics_tacet.valueChanged.connect(self.dataChanged)
self.ui.dynamics_fp.valueChanged.connect(self.dataChanged)
self.ui.dynamics_sp.valueChanged.connect(self.dataChanged)
self.ui.dynamics_spp.valueChanged.connect(self.dataChanged)
self.ui.dynamics_sfz.valueChanged.connect(self.dataChanged)
self.ui.dynamics_sf.valueChanged.connect(self.dataChanged)
self.ui.dynamics_sff.valueChanged.connect(self.dataChanged)
self.ui.buttonResetDurations.clicked.connect(lambda: api.resetDuationSettingsSignature(self.trackExportObject["id"]))
self.ui.buttonResetDynamics.clicked.connect(lambda: api.resetDynamicSettingsSignature(self.trackExportObject["id"]))
#Connect the hide checkbox
self.ui.advanced.toggled.connect(self.advancedToggled)
self.ui.advanced.setChecked(False)
def visibleToggled(self, signal):
assert signal == bool(self.ui.visibleCheckbox.checkState())
if signal:
api.unhideTrack(self.trackExportObject["id"])
else:
api.hideTrack(self.trackExportObject["id"])
def audibleToggled(self, signal):
assert signal == bool(self.ui.audibleCheckbox.checkState())
api.trackAudible(self.trackExportObject["id"], signal)
def doubleTrackToggled(self, signal):
api.setDoubleTrack(self.trackExportObject["id"], signal)
def advancedToggled(self, bool):
assert bool == self.ui.advanced.isChecked()
if bool:
self.ui.advancedContent.show()
else:
self.ui.advancedContent.hide()
@contextmanager
def blockUiSignals(self):
"""prevent loops by blocking all signals that react on changed states.
Revoked at the end of the calling function.
Strictly this is not needed for signals like .clicked() (compared to .toggled()) but it is
a good last line of defense to prevent future bugs. Those signal-loops are hard to track.
ADD ALL NEW SUB WIDGETS HERE!!!!! THIS HAPPENED TWICED ALREADY AND WAS A NIGHTMARE TO DEBUG!
SIMPLY USING self.blockSignals(True) HAS NO EFFECT BECAUSE THOSE ARE IN .ui.
self.setUpdatesEnabled(False) doesn't work as well.
self.children() does not return the right children either, self.ui() is a python object,
not a qt widget."""
for widget in self.ui.__dict__.values():
widget.blockSignals(True)
for checkbox in self.ccChannels.values():
checkbox.blockSignals(True)
yield
for widget in self.ui.__dict__.values():
widget.blockSignals(False)
for checkbox in self.ccChannels.values():
checkbox.blockSignals(False)
def callClickWidgetForUpbeat(self):
dialog = TickWidget(self.parentDataEditor, initValue = self.ui.upbeatSpinBox.value())
self.ui.upbeatSpinBox.setValue(dialog.ui.ticks.value())
def nameChanged(self):
"""When enter is pressed or focus is lost"""
t = self.ui.nameLineEdit.text()
name = self.ui.instrumentName.text()
short = self.ui.shortInstrumentName.text()
api.setTrackName(self.trackExportObject["id"], nameString = t, initialInstrumentName = name, initialShortInstrumentName = short)
def upbeatChanged(self):
"""When enter is pressed or focus is lost"""
v = self.ui.upbeatSpinBox.value()
api.setTrackUpbeat(self.trackExportObject["id"], v)
def dataChanged(self):
"""Our data changed. Send to Engine"""
with self.blockUiSignals():
dictionary = {}
dictionary["initialMidiChannel"] = self.ui.midiChannelSpinBox.value()-1
dictionary["initialMidiProgram"] = self.ui.midiProgramSpinBox.value()
dictionary["initialMidiBankMsb"] = self.ui.midiBankMsbSpinBox.value()
dictionary["initialMidiBankLsb"] = self.ui.midiBankLsbSpinBox.value()
dictionary["ccChannels"] = tuple(chanNum-1 for chanNum, checkbox in self.ccChannels.items() if checkbox.checkState()) #can be unsorted
dictionary["midiTranspose"] = self.ui.midiTransposeSpinBox.value()
dictionary["duration.defaultOn"] = self.ui.defaultOn.text()
dictionary["duration.defaultOff"] = self.ui.defaultOff.text()
dictionary["duration.staccatoOn"] = self.ui.staccatoOn.text()
dictionary["duration.staccatoOff"] = self.ui.staccatoOff.text()
dictionary["duration.tenutoOn"] = self.ui.tenutoOn.text()
dictionary["duration.tenutoOff"] = self.ui.tenutoOff.text()
dictionary["duration.legatoOn"] = self.ui.legatoOn.text()
dictionary["duration.legatoOff"] = self.ui.legatoOff.text()
dictionary["dynamics.ppppp"] = self.ui.dynamics_ppppp.value()
dictionary["dynamics.pppp"] = self.ui.dynamics_pppp.value()
dictionary["dynamics.ppp"] = self.ui.dynamics_ppp.value()
dictionary["dynamics.pp"] = self.ui.dynamics_pp.value()
dictionary["dynamics.p"] = self.ui.dynamics_p.value()
dictionary["dynamics.mp"] = self.ui.dynamics_mp.value()
dictionary["dynamics.mf"] = self.ui.dynamics_mf.value()
dictionary["dynamics.f"] = self.ui.dynamics_f.value()
dictionary["dynamics.ff"] = self.ui.dynamics_ff.value()
dictionary["dynamics.fff"] = self.ui.dynamics_fff.value()
dictionary["dynamics.ffff"] = self.ui.dynamics_ffff.value()
dictionary["dynamics.custom"] = self.ui.dynamics_custom.value()
dictionary["dynamics.tacet"] = self.ui.dynamics_tacet.value()
dictionary["dynamics.fp"] = self.ui.dynamics_fp.value()
dictionary["dynamics.sp"] = self.ui.dynamics_sp.value()
dictionary["dynamics.spp"] = self.ui.dynamics_spp.value()
dictionary["dynamics.sfz"] = self.ui.dynamics_sfz.value()
dictionary["dynamics.sf"] = self.ui.dynamics_sf.value()
dictionary["dynamics.sff"] = self.ui.dynamics_sff.value()
if not self.trackExportObject == dictionary: #checks for keys and values
api.setTrackSettings(self.trackExportObject["id"], dictionary)
#else: no change
def updateData(self, trackExportObject):
"""Receives api updates. Change GUI fields accordingly"""
self.trackExportObject = trackExportObject
with self.blockUiSignals():
self.trackExportObject = trackExportObject
self.ui.upbeatSpinBox.setValue(trackExportObject["upbeatInTicks"])
self.ui.nameLineEdit.setText(trackExportObject["name"])
self.ui.midiChannelSpinBox.setValue(trackExportObject["initialMidiChannel"]+1)
self.ui.midiProgramSpinBox.setValue(trackExportObject["initialMidiProgram"])
self.ui.midiBankMsbSpinBox.setValue(trackExportObject["initialMidiBankMsb"])
self.ui.midiBankLsbSpinBox.setValue(trackExportObject["initialMidiBankLsb"])
self.ui.midiTransposeSpinBox.setValue(trackExportObject["midiTranspose"])
self.ui.instrumentName.setText(trackExportObject["initialInstrumentName"])
self.ui.shortInstrumentName.setText(trackExportObject["initialShortInstrumentName"])
self.ui.defaultOn.setText(trackExportObject["duration.defaultOn"])
self.ui.defaultOff.setText(trackExportObject["duration.defaultOff"])
self.ui.staccatoOn.setText(trackExportObject["duration.staccatoOn"])
self.ui.staccatoOff.setText(trackExportObject["duration.staccatoOff"])
self.ui.tenutoOn.setText(trackExportObject["duration.tenutoOn"])
self.ui.tenutoOff.setText(trackExportObject["duration.tenutoOff"])
self.ui.legatoOn.setText(trackExportObject["duration.legatoOn"])
self.ui.legatoOff.setText(trackExportObject["duration.legatoOff"])
self.ui.dynamics_ppppp.setValue(trackExportObject["dynamics.ppppp"])
self.ui.dynamics_pppp.setValue(trackExportObject["dynamics.pppp"])
self.ui.dynamics_ppp.setValue(trackExportObject["dynamics.ppp"])
self.ui.dynamics_pp.setValue(trackExportObject["dynamics.pp"])
self.ui.dynamics_p.setValue(trackExportObject["dynamics.p"])
self.ui.dynamics_mp.setValue(trackExportObject["dynamics.mp"])
self.ui.dynamics_mf.setValue(trackExportObject["dynamics.mf"])
self.ui.dynamics_f.setValue(trackExportObject["dynamics.f"])
self.ui.dynamics_ff.setValue(trackExportObject["dynamics.ff"])
self.ui.dynamics_fff.setValue(trackExportObject["dynamics.fff"])
self.ui.dynamics_ffff.setValue(trackExportObject["dynamics.ffff"])
self.ui.dynamics_custom.setValue(trackExportObject["dynamics.custom"])
self.ui.dynamics_tacet.setValue(trackExportObject["dynamics.tacet"])
self.ui.dynamics_fp.setValue(trackExportObject["dynamics.fp"])
self.ui.dynamics_sp.setValue(trackExportObject["dynamics.sp"])
self.ui.dynamics_spp.setValue(trackExportObject["dynamics.spp"])
self.ui.dynamics_sfz.setValue(trackExportObject["dynamics.sfz"])
self.ui.dynamics_sf.setValue(trackExportObject["dynamics.sf"])
self.ui.dynamics_sff.setValue(trackExportObject["dynamics.sff"])
if trackExportObject["initialMidiProgram"] >= 0:
self.ui.midiBankMsbSpinBox.setEnabled(True)
self.ui.midiBankLsbSpinBox.setEnabled(True)
else:
self.ui.midiBankMsbSpinBox.setEnabled(False)
self.ui.midiBankLsbSpinBox.setEnabled(False)
#hidden position can be 0 as well. So don't bool, check for None explicitely.
if trackExportObject["hiddenPosition"] is None: #visible
self.ui.visibleCheckbox.setChecked(True)
self.ui.deleteButton.setEnabled(True)
else:
self.ui.visibleCheckbox.setChecked(False)
self.ui.deleteButton.setEnabled(False) #You cannot delete hidden tracks. see api.deleteTrack
self.ui.doubleTrackCheckbox.setChecked(trackExportObject["double"])
self.ui.audibleCheckbox.setChecked(trackExportObject["audible"])
for i in range(0,16): #without 16
#Engine from 0 to 15, GUI from 1 to 16.
self.ccChannels[i+1].setChecked(i in trackExportObject["ccChannels"]) #a checkbox widget.
class TrackEditor(QtWidgets.QWidget):
"""Created by ControlMainWindow. One permanent instance"""
def __init__(self, mainWindow):
super().__init__()
self.mainWindow = mainWindow
self.layout = QtWidgets.QVBoxLayout()
self.layout.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
self.setLayout(self.layout)
api.callbacks.tracksChangedIncludingHidden.append(self.updateTrackWidgets)
self.tracks = {} #id:trackWidget. As any dict, not in order
#Upbeat Tick Widget and Action-Button
allUpbeats = QtWidgets.QWidget()
allUpbeatsLayout = QtWidgets.QHBoxLayout()
allUpbeatsLayout.setAlignment(QtCore.Qt.AlignTop | QtCore.Qt.AlignLeft)
allUpbeats.setLayout(allUpbeatsLayout)
self.layout.addWidget(allUpbeats)
self.allUpbeatsCombinedTickWidget = CombinedTickWidget()
allUpbeatsPushButton = QtWidgets.QPushButton(translate("trackEditorPythonFile", "set all upbeats"))
allUpbeatsPushButton.clicked.connect(self.setAllUpbeats) #gets the value itself
allUpbeatsLayout.addWidget(self.allUpbeatsCombinedTickWidget)
allUpbeatsLayout.addWidget(allUpbeatsPushButton)
#Reset all Advanced Views
foldAllAdvanvced = QtWidgets.QPushButton(translate("trackEditorPythonFile", "fold all advanced"))
foldAllAdvanvced.clicked.connect(self.foldAllAdvanvced)
allUpbeatsLayout.addWidget(foldAllAdvanvced)
unfoldAllAdvanvced = QtWidgets.QPushButton(translate("trackEditorPythonFile", "unfold all advanced"))
unfoldAllAdvanvced.clicked.connect(self.unfoldAllAdvanced)
allUpbeatsLayout.addWidget(unfoldAllAdvanvced)
def updateTrackWidgets(self, listOfStaticTrackRepresentations):
"""React to the backend adding, deleting, hiding or moving tracks.
A track widget persists until the track gets deleted. No re-creation on every change. """
leftOver = list(self.tracks.keys())
visibleTracks = []
trackWidgetsInOrder = []
nameLineEditsInOrder = []
upbeatSpinBoxsInOrder = []
for trackExportObject in listOfStaticTrackRepresentations:
#First are all normal, visible tracks.
#After that the tracks have a property "hiddenPosition" for hidden tracks that need individual sorting.
if not trackExportObject["id"] in self.tracks: #A brand new track
widget = TrackWidget(self, trackExportObject)
self.tracks[trackExportObject["id"]] = widget
self.layout.insertWidget(-1, widget)
widget.setTitle("id: {}".format(trackExportObject["id"]))
else:
leftOver.remove(trackExportObject["id"])
self.tracks[trackExportObject["id"]].ui.deleteButton.setEnabled(True)
self.tracks[trackExportObject["id"]].blockSignals(True)
self.tracks[trackExportObject["id"]].updateData(trackExportObject) #This has a sideeffect of updating widgets, which will change backend data and trigger a recursive updateTrackWidgets. We need to block the signals. self.tracks has the track widgets.
self.tracks[trackExportObject["id"]].blockSignals(False)
nameLineEditsInOrder.append(self.tracks[trackExportObject["id"]].ui.nameLineEdit)
trackWidgetsInOrder.append(self.tracks[trackExportObject["id"]])
if trackExportObject["hiddenPosition"] is None:
visibleTracks.append(trackExportObject)
for trId in leftOver: #track still exist here but not in the backend. Delete.
w = self.tracks[trId]
del self.tracks[trId]
w.setParent(None)
w.close()
#Sort tracks as they are in the backend/score-view
for widget in trackWidgetsInOrder: #visibleTracks is sorted and does not have empty tracks.
self.layout.removeWidget(widget)
for widget in trackWidgetsInOrder:
#Re-Insert all, this time in order.
self.layout.insertWidget(-1, widget)
#Create tab order. Those work in pairs, linked list. a before b. And multiple of those pairs need to be in order itself.
if len(nameLineEditsInOrder) > 1:
for pair in pairwise(nameLineEditsInOrder):
self.setTabOrder(*pair) #takes exactly two arguments
if len(visibleTracks) == 1:
w = self.tracks[visibleTracks[0]["id"]]
w.ui.deleteButton.setEnabled(False) #it is not possible to delete the only visible track. The backend will prevent it but we don't even offer the choice here.
def setAllUpbeats(self):
for trackWidget in self.tracks.values():
trackWidget.ui.upbeatSpinBox.setValue(self.allUpbeatsCombinedTickWidget.value())
def foldAllAdvanvced(self):
for trackWidget in self.tracks.values():
trackWidget.ui.advanced.setChecked(False)
def unfoldAllAdvanced(self):
for trackWidget in self.tracks.values():
trackWidget.ui.advanced.setChecked(True)
def keyPressEvent(self, event):
"""Escape closes the track editor"""
k = event.key() #49=1, 50=2 etc.
if k == QtCore.Qt.Key_Escape:
self.mainWindow.ui.actionData_Editor.setChecked(False)
self.mainWindow.toggleMainView()
super().keyPressEvent(event)
"""
api.callbacks.updateBlockTrack.append(lambda trId, blocksExportData: self.tracks[trId].blockScene.regenerateBlocks(blocksExportData))
class TrackBlockScene(QtWidgets.QGraphicsScene):
def __init__(self, parentWidget):
super().__init__()
self.blocks = []
self.parentWidget = parentWidget
def mousePressEvent(self, event):
button = event.button()
if button == 1: #left
self.setEnabled(False)
super().mousePressEvent(event)
def mouseReleaseEvent(self, event):
button = event.button()
if button == 1: #left
self.setEnabled(True)
super().mouseReleaseEvent(event)
def regenerateBlocks(self, blocksExportData):
self.clear()
self.blocks = []
dur = 0
for backendBlock in blocksExportData:
b = BlockGraphicsItem(self, backendBlock)
self.blocks.append(b)
self.addItem(b)
b.setPos(backendBlock["tickindex"] / constantsAndConfigs.blocksTicksToPixelRatio, 0)
dur += backendBlock["completeDuration"]
self.parentWidget.ui.blocks.setFixedSize(dur / constantsAndConfigs.blocksTicksToPixelRatio, 30)
class BlockGraphicsItem(QtWidgets.QGraphicsRectItem):
def __init__(self, parentTrackWidget, blockExportObject):
width = blockExportObject["completeDuration"] / constantsAndConfigs.blocksTicksToPixelRatio
super().__init__(0,0,width, 30) #x, y, w, h
self.setBrush(self.stringToColor(blockExportObject["name"]))
#self.setFlags(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges|QtWidgets.QGraphicsItem.ItemIsFocusable)
#self.setCursor(QtCore.Qt.SizeAllCursor)
def stringToColor(self, st):
if st:
c = md5(st.encode()).hexdigest()
return QtGui.QColor(int(c[0:9],16) % 255, int(c[10:19],16) % 255, int(c[20:29],16)% 255, 255)
else:
return QtGui.QColor(255,255,255,255) #Return White
"""