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.
652 lines
34 KiB
652 lines
34 KiB
#! /usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Copyright 2021, 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.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.
|
|
self.chooseCurrentTrack(api.session.data.tracks[0].export()) #By Grabthar's hammer, by the suns of Worvan, what a hack! #TODO: Access to the sessions data structure directly instead of api. Not good. Getter function or api @property is cleaner.
|
|
|
|
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):
|
|
"""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.
|
|
|
|
"""
|
|
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.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
|
|
|
|
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)
|
|
|
|
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)
|
|
|
|
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 beccome 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):
|
|
#We just receive an int, not a dict.
|
|
beatsPerMinute.blockSignals(True)
|
|
bpmCheckbox.blockSignals(True)
|
|
|
|
if newValue:
|
|
bpmCheckbox.setChecked(True)
|
|
beatsPerMinute.setEnabled(True)
|
|
beatsPerMinute.setValue(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):
|
|
"""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 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.addSeparator("menuEdit")
|
|
|
|
self.menu.orderSubmenus(["menuFile", "menuEdit", "menuView", "menuHelp", "menuDebug"])
|
|
|