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.
 
 

679 lines
36 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 ),
This 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/>.
"""
import logging; logger = logging.getLogger(__name__); logger.info("import")
#Standard Library Modules
import os.path
#Third Party Modules
from PyQt5 import QtWidgets, QtCore, QtGui
#Template Modules
from template.qtgui.mainwindow import MainWindow as TemplateMainWindow
from template.qtgui.menu import Menu
from template.qtgui.about import About
#Our modules
import engine.api as api
from .songeditor import SongEditor, TrackLabelEditor
from .timeline import Timeline
from .pattern_grid import PatternGrid, VelocityControls, TransposeControls
from .resources import *
from .submenus import convertSubdivisionsSubMenu, globalOffsetSubmenu
MAX_QT_SIZE = 2147483647-1
class MainWindow(TemplateMainWindow):
#Undo/Redo translations by matching strings from the api
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Tempo")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Group Duration")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Steps per Pattern")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Group Size")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Convert Grouping")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Swing")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Measures per Track")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Measures per Group")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Track Name")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Track Color")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Track Midi Channel")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Add Track")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Clone Track")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Add deleted Track again")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Delete Track and autocreated Track")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Delete Track")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Move Track")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Pattern Multiplier")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Set Measures")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Invert Measures")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Track Measures Off")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Track Measures On")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Copy Measures")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Replace Measures")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Set Modal Shift")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Set Half Tone Shift")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Change Group")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Insert/Duplicate Group")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Exchange Group Order")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Clear all Group Transpositions")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Delete whole Group")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Change Step")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Remove Step")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Set Scale")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Note Names")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Transpose Scale")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Invert Steps")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "All Steps On")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "All Steps Off")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Invert Row")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Clear Row")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Fill Row with Repeat")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Change Row Velocity")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Change Pattern Velocity")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Number of Notes in Pattern")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Global Rhythm Offset")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Track Group")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Move Group")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Set Step Delay")
QtCore.QT_TRANSLATE_NOOP("NOOPengineHistory", "Set Augmentation Factor")
def __init__(self):
"""The order of calls is very important.
The split ploint is calling the super.__init. Some functions need to be called before,
some after.
For example:
The about dialog is created in the template main window init. So we need to set additional
help texts before that init.
"""
#Inject more help texts in the templates About "Did You Know" field.
#About.didYouKnow is a class variable.
#Make the first three words matter!
#Do not start them all with "You can..." or "...that you can", in response to the Did you know? title.
#We use injection into the class and not a parameter because this dialog gets shown by creating an object. We can't give the parameters when this is shown via the mainWindow menu.
About.didYouKnow = [
QtCore.QCoreApplication.translate("About", "Prefer clone track over adding a new empty track when creating a new pattern for an existing 'real world' instrument."),
QtCore.QCoreApplication.translate("About", "You can run multiple Patroneo instances in parallel to create complex polyrhythms."),
QtCore.QCoreApplication.translate("About", "To revert all steps that are longer or shorter than default invert the pattern twice in a row."),
QtCore.QCoreApplication.translate("About", "Control a synth with MIDI Control Changes (CC) by routing a Patroneo track into a midi plugin that converts notes to CC."),
QtCore.QCoreApplication.translate("About", "Many elements have context menus with unique functions: Try right clicking on a Step, the Track name or a measure in the song editor."),
] + About.didYouKnow
super().__init__()
#New menu entries and template-menu overrides
self.createMenu() # in its own function for readability
#Playback Controls
width = 65
self.ui.playPauseButton.setFixedWidth(width)
self.ui.playPauseButton.setText("")
self.ui.playPauseButton.setIcon(QtGui.QIcon(':playpause.png'))
self.ui.playPauseButton.clicked.connect(api.playPause)
self.ui.playPauseButton.setToolTip(QtCore.QCoreApplication.translate("PlaybackControls", "[Space] Play / Pause"))
self.ui.playPauseButton.setShortcut("Space")
#self.ui.centralwidget.addAction(self.ui.actionPlayPause) #no action without connection to a widget.
#self.ui.actionPlayPause.triggered.connect(self.ui.playPauseButton.click)
self.ui.loopButton.setFixedWidth(width)
self.ui.loopButton.setText("")
self.ui.loopButton.setIcon(QtGui.QIcon(':loop.png'))
self.ui.loopButton.setToolTip(QtCore.QCoreApplication.translate("PlaybackControls", "[L] Loop current Measure"))
self.ui.loopButton.clicked.connect(api.toggleLoop)
self.ui.loopButton.setShortcut("l")
#self.ui.centralwidget.addAction(self.ui.actionLoop) #no action without connection to a widget.
#self.ui.actionLoop.triggered.connect(self.ui.loopButton.click)
def callback_loopButtonText(measureNumber):
if not measureNumber is None:
nrstr = str(measureNumber+1)
self.ui.loopButton.setText(nrstr)
else:
self.ui.loopButton.setText("")
self.ui.loopButton.setShortcut("l") #Qt buttons lose their shortcut after setText
api.callbacks.loopChanged.append(callback_loopButtonText)
self.ui.loopMeasureFactorSpinBox.setFixedWidth(width)
self.ui.loopMeasureFactorSpinBox.setToolTip(QtCore.QCoreApplication.translate("PlaybackControls", "Number of measures in the loop"))
self.ui.loopMeasureFactorSpinBox.valueChanged.connect(self._sendLoopMeasureFactor)
api.callbacks.loopMeasureFactorChanged.append(self.ui.loopMeasureFactorSpinBox.setValue) #at least on load
self.ui.toStartButton.setFixedWidth(width)
self.ui.toStartButton.setText("")
self.ui.toStartButton.setIcon(QtGui.QIcon(':tostart.png'))
self.ui.toStartButton.setToolTip(QtCore.QCoreApplication.translate("PlaybackControls", "[Home] Jump to Start"))
self.ui.toStartButton.setShortcut("Home")
self.ui.toStartButton.clicked.connect(api.rewind)
#self.ui.centralwidget.addAction(self.ui.actionToStart) #no action without connection to a widget.
#self.ui.actionToStart.triggered.connect(self.ui.toStartButton.click)
self._blockCurrentTrackSignal = False # to prevent recursive calls
self.currentTrackId = None #this is purely a GUI construct. the engine does not know a current track. On startup there is no active track
##Song Editor
self.ui.songEditorView.parentMainWindow = self
self.songEditor = SongEditor(parentView=self.ui.songEditorView)
self.ui.songEditorView.setScene(self.songEditor)
self.ui.songEditorView.setViewport(QtWidgets.QOpenGLWidget())
self.ui.trackEditorView.parentMainWindow = self
self.trackLabelEditor = TrackLabelEditor(parentView=self.ui.trackEditorView)
self.ui.trackEditorView.setScene(self.trackLabelEditor)
self.ui.trackEditorView.setViewport(QtWidgets.QOpenGLWidget())
self.ui.timelineView.parentMainWindow = self
self.timeline = Timeline(parentView=self.ui.timelineView)
self.ui.timelineView.setScene(self.timeline)
self.ui.timelineView.setViewport(QtWidgets.QOpenGLWidget())
#Sync the vertical trackEditorView scrollbar (which is never shown) with the songEditorView scrollbar.
self.ui.songEditorView.setVerticalScrollBar(self.ui.trackEditorView.verticalScrollBar()) #this seems backwards, but it is correct :)
#Sync the horizontal timelineView scrollbar (which is never shown) with the songEditorView scrollbar.
self.ui.songEditorView.setHorizontalScrollBar(self.ui.timelineView.horizontalScrollBar()) #this seems backwards, but it is correct :)
##Pattern Editor
self.ui.gridView.parentMainWindow = self
self.patternGrid = PatternGrid(parentView=self.ui.gridView)
self.ui.gridView.setScene(self.patternGrid)
self.ui.gridView.setRenderHints(QtGui.QPainter.TextAntialiasing)
self.ui.gridView.setViewport(QtWidgets.QOpenGLWidget()) #TODO: QT BUG! Review in the far future.
#ProxyWidget-Items in an openGl accel. GraphicsView create a ton of messages: Unsupported composition mode
#But everything works. Install a message handler to just get rid of the warning.
def passHandler(msg_type, msg_log_context, msg_string):
pass
QtCore.qInstallMessageHandler(passHandler)
self.patternToolbar = QtWidgets.QToolBar()
self.ui.patternArea.layout().insertWidget(0, self.patternToolbar)
self._populatePatternToolbar()
#Toolbar, which needs the widgets above already established
self._populateToolbar()
#Statusbar will show possible actions, such as "use scrollwheel to transpose"
#self.statusBar().showMessage(QtCore.QCoreApplication.translate("Statusbar", ""))
self.statusBar().showMessage("")
api.session.data.setLanguageForEmptyFile(language = QtCore.QLocale().languageToString(QtCore.QLocale().language())) #TODO: this is a hack because we access the session directly. But this is also a function tied to Qts language string. Two wrongs...
self.start() #This shows the GUI, or not, depends on the NSM gui save setting. We need to call that after the menu, otherwise the about dialog will block and then we get new menu entries, which looks strange.
#There is always a track. Forcing that to be active is better than having to hide all the pattern widgets, or to disable them.
#However, we need the engine to be ready.
#Now in 2021-12-13 this is still necessary because of regressions! Eventhough the API sends a current track on startup as convenience for the GUI and hardware controllers
self.chooseCurrentTrack(api.session.data.tracks[0].export(), sendChangeToApi=False) #By Grabthar's hammer, by the suns of Worvan, what a hack!
api.callbacks.currentTrackChanged.append(self.chooseCurrentTrack)
self.ui.gridView.horizontalScrollBar().setSliderPosition(0)
self.ui.gridView.verticalScrollBar().setSliderPosition(0)
#There is so much going on in the engine, we never reach a save status on load.
#Here is the crowbar-method.
self.nsmClient.announceSaveStatus(isClean = True)
def _sendLoopMeasureFactor(self, *args):
self.ui.loopMeasureFactorSpinBox.blockSignals(True)
api.setLoopMeasureFactor(self.ui.loopMeasureFactorSpinBox.value())
self.ui.loopMeasureFactorSpinBox.blockSignals(False)
def chooseCurrentTrack(self, exportDict, sendChangeToApi=False):
"""This is in mainWindow because we need access to different sections of the program.
newCurrentTrack is a backend track ID
This is not triggered by the engine but by our GUI functions. exportDict is not the current
engine data but a cached version. Careful! For example the pattern redraws when the
current track changes, but there was no callback that informed the songeditor of a changed
pattern because it doesn't deal with patterns. So it didn't receive the new exportDict and
sent its old cached version to the patternGrid via this function. So the grid never
got the new information and "forgot" all settings.
2 years later and I don't understand the docstring anymore.
In 2021 this changed. The api now knows the current track to communicate with hardware
button pad controllers like the APCmini.
"""
if self._blockCurrentTrackSignal:
return
newCurrentTrackId = exportDict["id"]
if self.currentTrackId == newCurrentTrackId:
return True
participantsDicts = (self.trackLabelEditor.tracks, self.songEditor.tracks) #all structures with dicts with key trackId that need active/inactive marking
for d in participantsDicts:
try: #First mark old one inactive
d[self.currentTrackId].mark(False)
except KeyError: #track was deleted or never existed (load empty file)
pass
#d[newCurrentTrackId].mark(True) #New one as active
self.trackLabelEditor.tracks[newCurrentTrackId].mark(True) #New one as active
self.songEditor.tracks[newCurrentTrackId].mark(True) #New one as active
self.patternGrid.guicallback_chooseCurrentTrack(exportDict, newCurrentTrackId)
self.transposeControls.guicallback_chooseCurrentTrack(exportDict, newCurrentTrackId)
#Remember current one for next round and for other functions
#Functions depend on getting set after getting called. They need to know the old track!
self.currentTrackId = newCurrentTrackId
if sendChangeToApi:
self._blockCurrentTrackSignal = True
api.changeCurrentTrack(newCurrentTrackId)
self._blockCurrentTrackSignal = False
def addPatternTrack(self):
"""Add a new track and initialize it with some data from the current one"""
scale = api.session.data.trackById(self.currentTrackId).pattern.scale #TODO: Access to the sessions data structure directly instead of api. Not good. Getter function or api @property is cleaner.
newTrackId = api.addTrack(scale)
self.chooseCurrentTrack(self.songEditor.tracks[newTrackId].exportDict, sendChangeToApi=True)
def cloneSelectedTrack(self):
"""Add a new track via the template option. This is the best track add function"""
newTrackExporDict = api.createSiblingTrack(self.currentTrackId)
self.chooseCurrentTrack(newTrackExporDict, sendChangeToApi=True)
def _populatePatternToolbar(self):
"""Called once at the creation of the GUI"""
self.patternToolbar.contextMenuEvent = api.nothing #remove that annoying Checkbox context menu
spacerItemLeft = QtWidgets.QWidget()
spacerItemLeft.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
spacerItemRight = QtWidgets.QWidget()
spacerItemRight.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
self.patternToolbar.addWidget(spacerItemLeft)
#spacerItemRight is added as last widget
#Actual widgets
velocityControls = VelocityControls(mainWindow=self, patternScene=self.patternGrid)
self.patternToolbar.addWidget(velocityControls)
transposeControls = TransposeControls(parentScene=self.patternGrid)
self.transposeControls = transposeControls
self.patternToolbar.addWidget(transposeControls)
#Finally add a spacer to center all widgets
self.patternToolbar.addWidget(spacerItemRight)
def _populateToolbar(self):
"""Called once at the creation of the GUI"""
self.ui.toolBar.contextMenuEvent = api.nothing #remove that annoying Checkbox context menu
#Designer Actions
self.ui.actionAddPattern.triggered.connect(self.addPatternTrack)
self.ui.actionClone_Selected_Track.triggered.connect(self.cloneSelectedTrack)
#New Widgets. Toolbar in Designer can only have QActions, while in reality it can hold any widget. So we need to add them in code:
#We first define then, then add them below.
#BPM. Always in quarter notes to keep it simple
beatsPerMinuteBlock = QtWidgets.QWidget()
bpmLayout = QtWidgets.QHBoxLayout()
bpmLayout.setSpacing(0)
bpmLayout.setContentsMargins(0,0,0,0)
beatsPerMinuteBlock.setLayout(bpmLayout)
bpmCheckbox = QtWidgets.QCheckBox(QtCore.QCoreApplication.translate("Toolbar", "BPM/Tempo: "))
bpmCheckbox.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Deactivate to become JACK Transport Slave. Activate for Master."))
bpmLayout.addWidget(bpmCheckbox)
beatsPerMinute = QtWidgets.QSpinBox()
beatsPerMinute.setToolTip(bpmCheckbox.toolTip())
beatsPerMinute.setMinimum(0) #0 means off
beatsPerMinute.setMaximum(999)
beatsPerMinute.setSpecialValueText("JACK")
bpmLayout.addWidget(beatsPerMinute)
def quarterNotesPerMinuteChanged():
beatsPerMinute.blockSignals(True)
assert bpmCheckbox.isChecked()
value = beatsPerMinute.value()
api.set_quarterNotesPerMinute(max(1, value))
beatsPerMinute.blockSignals(False)
beatsPerMinute.editingFinished.connect(quarterNotesPerMinuteChanged)
def bpmCheckboxChanged(state):
bpmCheckbox.blockSignals(True)
if state:
api.set_quarterNotesPerMinute("on")
else:
api.set_quarterNotesPerMinute(None)
bpmCheckbox.blockSignals(False)
bpmCheckbox.stateChanged.connect(bpmCheckboxChanged)
def callback_quarterNotesPerMinuteChanged(newValue:float):
"""We just receive a float, not a dict. Jack supports floats as tempo.
However, we only use ints here and our spinbox is int."""
beatsPerMinute.blockSignals(True)
bpmCheckbox.blockSignals(True)
if newValue:
bpmCheckbox.setChecked(True)
beatsPerMinute.setEnabled(True)
beatsPerMinute.setValue(int(newValue))
else:
beatsPerMinute.setEnabled(False)
bpmCheckbox.setChecked(False)
beatsPerMinute.setValue(0)
beatsPerMinute.blockSignals(False)
bpmCheckbox.blockSignals(False)
api.callbacks.quarterNotesPerMinuteChanged.append(callback_quarterNotesPerMinuteChanged)
#Number of Measures
numberOfMeasures = QtWidgets.QSpinBox()
numberOfMeasures.setMinimum(1)
numberOfMeasures.setMaximum(9999)
numberOfMeasures.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Overall length of the song"))
def numberOfMeasuresChanged():
numberOfMeasures.blockSignals(True)
api.set_numberOfMeasures(int(numberOfMeasures.value()))
numberOfMeasures.blockSignals(False)
numberOfMeasures.editingFinished.connect(numberOfMeasuresChanged)
def callback_setnumberOfMeasures(exportDictScore):
numberOfMeasures.blockSignals(True)
numberOfMeasures.setValue(exportDictScore["numberOfMeasures"])
numberOfMeasures.blockSignals(False)
api.callbacks.numberOfMeasuresChanged.append(callback_setnumberOfMeasures)
#Subdivisions
#See manual
numberOfSubdivisions = QtWidgets.QSpinBox()
numberOfSubdivisions.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Please read the manual!"))
self.numberOfSubdivisions = numberOfSubdivisions #access from the convert dialog
numberOfSubdivisions.setMinimum(1)
numberOfSubdivisions.setMaximum(4)
def numberOfSubdivisionsChanged():
api.set_subdivisions(numberOfSubdivisions.value())
numberOfSubdivisions.editingFinished.connect(numberOfSubdivisionsChanged)
def callback_subdivisionsChanged(newValue):
numberOfSubdivisions.blockSignals(True)
numberOfSubdivisions.setValue(newValue)
numberOfSubdivisions.blockSignals(False)
api.callbacks.subdivisionsChanged.append(callback_subdivisionsChanged)
#Time Signature
unitTypes = [
QtCore.QCoreApplication.translate("TimeSignature", "Whole"),
QtCore.QCoreApplication.translate("TimeSignature", "Half"),
QtCore.QCoreApplication.translate("TimeSignature", "Quarter"),
QtCore.QCoreApplication.translate("TimeSignature", "Eigth"),
QtCore.QCoreApplication.translate("TimeSignature", "Sixteenth")
]
units = [api.D1, api.D2, api.D4, api.D8, api.D16]
howManyUnits = QtWidgets.QSpinBox()
howManyUnits.setMinimum(1)
howManyUnits.setMaximum(999)
howManyUnits.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Length of the pattern (bottom part of the program)"))
whatTypeOfUnit = QtWidgets.QComboBox()
whatTypeOfUnit.addItems(unitTypes)
#whatTypeOfUnit.setStyleSheet("QComboBox { background-color: transparent; } QComboBox QAbstractItemView { selection-background-color: transparent; } ");
whatTypeOfUnit.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "How long is each main step"))
def typeChanged(index):
whatTypeOfUnit.blockSignals(True)
api.set_whatTypeOfUnit(units[index])
whatTypeOfUnit.blockSignals(False)
whatTypeOfUnit.currentIndexChanged.connect(typeChanged)
def numberChanged():
howManyUnits.blockSignals(True)
api.set_howManyUnits(howManyUnits.value())
howManyUnits.blockSignals(False)
howManyUnits.editingFinished.connect(numberChanged)
def callback_setTimeSignature(nr, typ):
howManyUnits.blockSignals(True)
whatTypeOfUnit.blockSignals(True)
idx = units.index(typ)
whatTypeOfUnit.setCurrentIndex(idx)
howManyUnits.setValue(nr)
howManyUnits.blockSignals(False)
whatTypeOfUnit.blockSignals(False)
api.callbacks.timeSignatureChanged.append(callback_setTimeSignature)
#Swing Controls
swingControls = QtWidgets.QSlider()
swingControls.setOrientation(1)
swingControls.setMinimum(0)
swingControls.setMaximum(100)
#swingControls.setAutoFillBackground(True)
op=QtWidgets.QGraphicsOpacityEffect(self)
swingControls.setGraphicsEffect(op)
swingControls.setMaximumSize(200, 40) #w, h. We don't care about h
opL=QtWidgets.QGraphicsOpacityEffect(self)
swingLabel = QtWidgets.QLabel("")
swingLabel.setGraphicsEffect(opL)
def handleCallback_swingPercentChanged(value):
swingLabel.setText(f"{value}% Swing:") #no translation.
swingControls.blockSignals(True)
swingControls.setValue(value)
swingControls.blockSignals(False)
api.callbacks.swingPercentChanged.append(handleCallback_swingPercentChanged)
def swingControls_ValueChanged():
"""Our own gui change that gets send to the engine"""
swingControls.blockSignals(True)
api.setSwingPercent(swingControls.value())
swingControls.blockSignals(False)
swingControls.valueChanged.connect(swingControls_ValueChanged)
def swingControls_Subdivisions(value):
"""Engine allows swing only for subdivision grouping 2 and 4.
Switch off the slider"""
if not value % 2 == 0:
#Hide or setVisible does not work here?!!??! the widget only flickers
op.setOpacity(0.00) #fuck it. Workaround for hide.
opL.setOpacity(0.00)
swingControls.setEnabled(False)
else:
op.setOpacity(1.00)
opL.setOpacity(1.00)
swingControls.setEnabled(True)
api.callbacks.subdivisionsChanged.append(swingControls_Subdivisions)
#Add all to Toolbar
def spacer():
"""Insert a spacing widget. positions depends on execution order"""
spacer = QtWidgets.QWidget()
spacer.setFixedWidth(5)
self.ui.toolBar.addWidget(spacer)
#Clone Track and addPatternTrack button is added through Designer but we change the text here to get a translation
self.ui.actionClone_Selected_Track.setText(QtCore.QCoreApplication.translate("Toolbar", "Clone Selected Track"))
self.ui.actionClone_Selected_Track.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Use this! Create a new track that inherits everything but the content from the original. Already jack connected!"))
self.ui.actionAddPattern.setText(QtCore.QCoreApplication.translate("Toolbar", "Add Track"))
self.ui.actionAddPattern.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Add a complete empty track that needs to be connected to an instrument manually."))
spacer()
self.ui.toolBar.addWidget(beatsPerMinuteBlock) # combined widget with its label and translation included
spacer()
l = QtWidgets.QLabel(QtCore.QCoreApplication.translate("Toolbar", "Measures per Track: "))
l.setToolTip(numberOfMeasures.toolTip())
self.ui.toolBar.addWidget(l)
self.ui.toolBar.addWidget(numberOfMeasures)
spacer()
l = QtWidgets.QLabel(QtCore.QCoreApplication.translate("Toolbar", "Steps per Pattern:"))
l.setToolTip(howManyUnits.toolTip())
self.ui.toolBar.addWidget(l)
self.ui.toolBar.addWidget(howManyUnits)
l = QtWidgets.QLabel(QtCore.QCoreApplication.translate("Toolbar", " in groups of: "))
l.setToolTip(howManyUnits.toolTip())
self.ui.toolBar.addWidget(l)
self.ui.toolBar.addWidget(numberOfSubdivisions)
l = QtWidgets.QLabel(QtCore.QCoreApplication.translate("Toolbar", " so that each group produces a:"))
l.setToolTip(whatTypeOfUnit.toolTip())
self.ui.toolBar.addWidget(l)
self.ui.toolBar.addWidget(whatTypeOfUnit)
spacer()
swingLabel.setToolTip(QtCore.QCoreApplication.translate("Toolbar", "Set the swing factor. 0 is off. Different effect for different rhythm-grouping!"))
self.ui.toolBar.addWidget(swingLabel)
self.ui.toolBar.addWidget(swingControls)
spacer()
def zoom(self, scaleFactor:float):
pass
def stretchXCoordinates(self, factor):
pass
def zoomUpperHalf(self, event):
"""This is called from within the parts of the combined song editor.
The Song Editor consists of three graphic scenes and their views.
But only the part where you can switch measures on and off calls this."""
try: self.zoomFactor
except: self.zoomFactor = 1 # no save. We don't keep a qt config.
delta = event.delta()
if delta > 0: #zoom in
self.zoomFactor = min(5, round(self.zoomFactor + 0.25, 2))
else: #zoom out
self.zoomFactor = max(1, round(self.zoomFactor - 0.25, 2))
for view in (self.ui.songEditorView, self.ui.trackEditorView, self.ui.timelineView):
view.resetTransform()
self.ui.songEditorView.scale(self.zoomFactor, self.zoomFactor)
self.ui.trackEditorView.scale(1, self.zoomFactor)
self.ui.timelineView.scale(self.zoomFactor, 1)
self.ui.songEditorView.centerOn(event.scenePos())
#self.ui.trackEditorView.verticalScrollBar().setValue(event.scenePos().y())
#self.ui.timelineView.horizontalScrollBar().setValue(event.scenePos().x())
def _stretchXCoordinates(*args): pass #Override template function
def maximizeSongArea(self):
self.ui.patternArea.setMinimumSize(1, 1)
self.ui.splitter.setSizes([MAX_QT_SIZE, 1])
def maximizePatternArea(self):
self.ui.songArea.setMinimumSize(1, 1)
self.ui.splitter.setSizes([1, MAX_QT_SIZE])
def equalizeSongPatternAreas(self):
self.ui.splitter.setSizes([1,1])
def toggleFollowPlayheadInPattern(self):
"""Toggling is done by Qt. We do not need to do anything here at the moment. Pattern.playhead will just check
the menu checkbox"""
pass
#now = self.ui.actionPatternFollowPlayhead.isChecked()
#self.ui.actionPatternFollowPlayhead.setChecked(not now)
def toggleVelocitiesAlwaysVisible(self):
"""Toggling is done by Qt."""
#now = self.ui.actionVelocitiesAlwaysVisible.isChecked()
#self.ui.actionVelocitiesAlwaysVisible.setChecked(not now)
self.patternGrid.decideYourselfIToShowVelocities()
def halftoneTranspose(self, value):
if self.songEditor.currentHoverStep and self.songEditor.currentHoverStep.isVisible():
if value == 1: #only +1 and -1 for the menu action.
self.songEditor.currentHoverStep.increaseHalftoneTranspose()
else:
self.songEditor.currentHoverStep.decreaseHalftoneTranspose()
def scaleTranspose(self, value):
"""Value is 1 and -1, simply to indicate a direction. In reality this is halving and doubling"""
if self.songEditor.currentHoverStep and self.songEditor.currentHoverStep.isVisible():
if value == 1:
self.songEditor.currentHoverStep.increaseScaleTranspose()
else:
self.songEditor.currentHoverStep.decreaseScaleTranspose()
def stepDelay(self, value):
if self.songEditor.currentHoverStep and self.songEditor.currentHoverStep.isVisible():
if value == 1: #only +1 and -1 for the menu action.
self.songEditor.currentHoverStep.increaseStepDelay()
else:
self.songEditor.currentHoverStep.decreaseStepDelay()
def augmentationFactor(self, value):
"""Value is 1 and -1, simply to indicate a direction. In reality this is halving and doubling"""
if self.songEditor.currentHoverStep and self.songEditor.currentHoverStep.isVisible():
if value == 1:
self.songEditor.currentHoverStep.increaseAugmentationFactor()
else:
self.songEditor.currentHoverStep.decreaseAugmentationFactor()
def createMenu(self):
#We have undo/redo since v2.1. Template menu entries were hidden before.
#self.ui.actionUndo.setVisible(False)
#self.ui.actionRedo.setVisible(False)
self.menu.addMenuEntry("menuEdit", "actionConvertSubdivisions", QtCore.QCoreApplication.translate("Menu", "Convert Grouping"), lambda: convertSubdivisionsSubMenu(self), tooltip=QtCore.QCoreApplication.translate("Menu", "Change step-grouping but keep your music the same"))
self.menu.addMenuEntry("menuEdit", "actionGlobalOffsetSubmenu", QtCore.QCoreApplication.translate("Menu", "Global Rhythm Offset"), lambda: globalOffsetSubmenu(self), tooltip=QtCore.QCoreApplication.translate("Menu", "Shift the whole piece further down the timeline"))
self.menu.addSeparator("menuEdit")
#Measure Modifications
self.menu.addMenuEntry("menuEdit", "actionHalftoneTransposeIncrease", QtCore.QCoreApplication.translate("Menu", "Increase halftone transpose for currently hovered measure (use shortcut!)"), lambda: self.halftoneTranspose(1), shortcut="h")
self.menu.addMenuEntry("menuEdit", "actionHalftoneTransposeDecrease", QtCore.QCoreApplication.translate("Menu", "Decrease halftone transpose for currently hovered measure (use shortcut!)"), lambda: self.halftoneTranspose(-1), shortcut="Shift+h")
self.menu.addMenuEntry("menuEdit", "actionScaleTransposeIncrease", QtCore.QCoreApplication.translate("Menu", "Increase in-scale transpose for currently hovered measure (use shortcut!)"), lambda: self.scaleTranspose(1), shortcut="s")
self.menu.addMenuEntry("menuEdit", "actionScaleTransposeDecrease", QtCore.QCoreApplication.translate("Menu", "Decrease in-scale transpose for currently hovered measure (use shortcut!)"), lambda: self.scaleTranspose(-1), shortcut="Shift+s")
self.menu.addMenuEntry("menuEdit", "actionStepDelayIncrease", QtCore.QCoreApplication.translate("Menu", "Increase step delay for currently hovered measure (use shortcut!)"), lambda: self.stepDelay(1), shortcut="d")
self.menu.addMenuEntry("menuEdit", "actionStepDelayDecrease", QtCore.QCoreApplication.translate("Menu", "Decrease step delay for currently hovered measure (use shortcut!)"), lambda: self.stepDelay(-1), shortcut="Shift+d")
self.menu.addMenuEntry("menuEdit", "actionAugmentationFactorIncrease", QtCore.QCoreApplication.translate("Menu", "Increase augmentation factor for currently hovered measure (use shortcut!)"), lambda: self.augmentationFactor(1), shortcut="a")
self.menu.addMenuEntry("menuEdit", "actionAugmentationFactorDecrease", QtCore.QCoreApplication.translate("Menu", "Decrease augmentation factor for currently hovered measure (use shortcut!)"), lambda: self.augmentationFactor(-1), shortcut="Shift+a")
self.menu.addSubmenu("menuView", QtCore.QCoreApplication.translate("menu","View"))
self.menu.addMenuEntry("menuView", "actionMaximizeSongArea", QtCore.QCoreApplication.translate("menu", "Maximize Song Area"), self.maximizeSongArea)
self.menu.addMenuEntry("menuView", "actionMaximizePatternArea", QtCore.QCoreApplication.translate("menu", "Maximize Pattern Area"), self.maximizePatternArea)
self.menu.addMenuEntry("menuView", "actionEqualSizeSongPatternArea", QtCore.QCoreApplication.translate("menu", "Equal space for Pattern/Song Area"), self.equalizeSongPatternAreas)
self.menu.addMenuEntry("menuView", "actionPatternFollowPlayhead", QtCore.QCoreApplication.translate("menu", "Follow playhead in pattern-view by scrolling."), self.toggleFollowPlayheadInPattern, checkable=True, shortcut="f")
self.menu.addMenuEntry("menuView", "actionVelocitiesAlwaysVisible", QtCore.QCoreApplication.translate("menu", "Velocity numbers are always visible"), self.toggleVelocitiesAlwaysVisible, checkable=True, shortcut="v")
#self.menu.addSeparator("menuEdit")
self.menu.orderSubmenus(["menuFile", "menuEdit", "menuView", "menuHelp", "menuDebug"])