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.
288 lines
10 KiB
288 lines
10 KiB
2 years ago
|
#! /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")
|
||
|
|
||
|
|
||
|
#Third Party
|
||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||
|
translate = QtCore.QCoreApplication.translate
|
||
|
|
||
|
#Template Modules
|
||
|
|
||
|
#Our Modules
|
||
|
from .submenus import CombinedTickWidget
|
||
|
import engine.api as api
|
||
|
|
||
|
def nothing(*args):
|
||
|
pass
|
||
|
|
||
|
class CustomMetricalInstructionWidget(QtWidgets.QDialog):
|
||
|
def __init__(self, mainWindow):
|
||
|
"""Init is called everytime the dialog gets shown"""
|
||
|
super().__init__(mainWindow)
|
||
|
self.mainWindow = mainWindow
|
||
|
|
||
|
#Add the title as simple LabelWidget
|
||
|
self.mainLayout = QtWidgets.QVBoxLayout(self)
|
||
|
self.label = QtWidgets.QLabel(translate("CustomMetricalInstructionWidget", "Design your own Metrical Instruction. These are not just time signatures, please refer to the manual!"))
|
||
|
self.label.setWordWrap(True)
|
||
|
self.mainLayout.addWidget(self.label)
|
||
|
|
||
|
#All other Widgets are in a Formlayout
|
||
|
self.subFormLayout = QtWidgets.QFormLayout()
|
||
|
self.mainLayout.addLayout(self.subFormLayout)
|
||
|
|
||
|
#Lilypond Override, must be specified
|
||
|
self.lilypondOverride = QtWidgets.QLineEdit("\\time 4/4")
|
||
|
self.subFormLayout.addRow(translate("CustomMetricalInstructionWidget", "Simplified Lilypond"), self.lilypondOverride)
|
||
|
|
||
|
|
||
|
#Chain Measures Widget
|
||
|
#We use this to determine the number of measure-designer-widgets shown
|
||
|
self.chainMeasuresSpinBox = QtWidgets.QSpinBox()
|
||
|
self.chainMeasuresSpinBox.setMinimum(1)
|
||
|
self.chainMeasuresSpinBox.setMaximum(8) #arbitrary, but far beyond musical necessity
|
||
|
self.subFormLayout.addRow(translate("CustomMetricalInstructionWidget", "Chain how many measures?"), self.chainMeasuresSpinBox)
|
||
|
|
||
|
#Is Metrical?
|
||
|
#If False all dynamic playback modifications based on the metre will be deactivated but barlines will still be created.
|
||
|
self.isMetricalCheckBox = QtWidgets.QCheckBox()
|
||
|
self.isMetricalCheckBox.setChecked(True)
|
||
|
self.subFormLayout.addRow(translate("CustomMetricalInstructionWidget", "Rendered Meter?"), self.isMetricalCheckBox)
|
||
|
|
||
|
|
||
|
#We construct all sub widgets now and hide them instead of creating and deleting them on-the-fly
|
||
|
#This way they keep their values if temporarily removed through user input by reducing self.chainMeasuresSpinBox.value
|
||
|
self.measureWidgets = []
|
||
|
for i in range(self.chainMeasuresSpinBox.maximum()):
|
||
|
mw = MeasureWidget(self)
|
||
|
self.measureWidgets.append(mw)
|
||
|
self.mainLayout.addWidget(mw)
|
||
|
mw.hide()
|
||
|
|
||
|
#Set Values, trigger callbacks
|
||
|
self.chainMeasuresSpinBox.valueChanged.connect(self.adjustNumberOfMeasureWidgets)
|
||
|
self.chainMeasuresSpinBox.setValue(1)
|
||
|
self.adjustNumberOfMeasureWidgets(self.chainMeasuresSpinBox.value())
|
||
|
|
||
|
#Finally add ButtonBox to the main layout, not to the formlayout
|
||
|
self.buttonBox = QtWidgets.QDialogButtonBox()
|
||
|
self.okButton = self.buttonBox.addButton(QtWidgets.QDialogButtonBox.Ok)
|
||
|
self.cancelButton = self.buttonBox.addButton(QtWidgets.QDialogButtonBox.Cancel)
|
||
|
self.buttonBox.accepted.connect(self.process)
|
||
|
self.buttonBox.rejected.connect(self.reject)
|
||
|
self.mainLayout.addWidget(self.buttonBox)
|
||
|
|
||
|
self.exec()
|
||
|
|
||
|
|
||
|
def showEvent(self, event):
|
||
|
super().showEvent(event)
|
||
|
self.adjustSize()
|
||
|
|
||
|
|
||
|
def adjustNumberOfMeasureWidgets(self, newValue:int):
|
||
|
"""Just hide and show. The maximum is fixed."""
|
||
|
for mw in self.measureWidgets:
|
||
|
mw.hide()
|
||
|
|
||
|
for mw in self.measureWidgets[:newValue]:
|
||
|
mw.show()
|
||
|
|
||
|
self.adjustSize()
|
||
|
|
||
|
def sanityCheck(self):
|
||
|
"""Called by child measure widgets after any change.
|
||
|
Do not call mw.convert in here, this will be recursive!"""
|
||
|
self.okButton.setEnabled(True)
|
||
|
for mw in self.measureWidgets:
|
||
|
if not mw.isVisible():
|
||
|
break
|
||
|
if not mw.validState:
|
||
|
self.okButton.setEnabled(False)
|
||
|
|
||
|
def process(self):
|
||
|
"""Pressed ok"""
|
||
|
result = []
|
||
|
|
||
|
for mw in self.measureWidgets:
|
||
|
# All visible mw are + Compound Measure "Half Barline"
|
||
|
# Most common case is just one mw visible.
|
||
|
if not mw.isVisible():
|
||
|
break
|
||
|
con = mw.convert()
|
||
|
result.append(con)
|
||
|
|
||
|
#api.insertMetricalInstruction
|
||
|
self.done(True)
|
||
|
|
||
|
|
||
|
class MeasureWidget(QtWidgets.QGroupBox):
|
||
|
"""Sub Widget for the user to choose strong and weak positions"""
|
||
|
def __init__(self, parentWidget):
|
||
|
super().__init__(parentWidget)
|
||
|
|
||
|
self.maxLevel = 3
|
||
|
self.parentWidget = parentWidget
|
||
|
self.layout = QtWidgets.QVBoxLayout(self)
|
||
|
|
||
|
self.validState = True
|
||
|
|
||
|
#Container
|
||
|
self.subFormLayout = QtWidgets.QFormLayout()
|
||
|
self.layout.addLayout(self.subFormLayout)
|
||
|
|
||
|
#Positions Widget
|
||
|
self.howManyPositionsSpinBox = QtWidgets.QSpinBox()
|
||
|
self.howManyPositionsSpinBox.setMinimum(1)
|
||
|
self.howManyPositionsSpinBox.setMaximum(64) #arbitrary, but far beyond musical necessity
|
||
|
self.subFormLayout.addRow(translate("CustomMetricalInstructionWidget", "How many positions"), self.howManyPositionsSpinBox)
|
||
|
|
||
|
#Base Duration Widget. Must not be 0
|
||
|
self.baseDuration = CombinedTickWidget(parentWidget.mainWindow)
|
||
|
self.subFormLayout.addRow(translate("CustomMetricalInstructionWidget", "Base duration in ticks"), self.baseDuration)
|
||
|
|
||
|
#The position sliders
|
||
|
self.positionLayout = QtWidgets.QHBoxLayout()
|
||
|
self.layout.addLayout(self.positionLayout)
|
||
|
self.positionLayout.addStretch()
|
||
|
self.positions = []
|
||
|
for i in range(self.howManyPositionsSpinBox.maximum()):
|
||
|
|
||
|
sliderCombo = QtWidgets.QVBoxLayout()
|
||
|
self.positionLayout.addLayout(sliderCombo)
|
||
|
|
||
|
#Show position counter above
|
||
|
counterLabel = QtWidgets.QLabel(f"№{i+1}")
|
||
|
counterLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||
|
sliderCombo.addWidget(counterLabel)
|
||
|
counterLabel.hide()
|
||
|
|
||
|
#Vertical Slider in the Middle
|
||
|
line = QtWidgets.QSlider()
|
||
|
line.setMinimum(0)
|
||
|
if i==0:
|
||
|
line.setMaximum(self.maxLevel+1)
|
||
|
line.setValue(self.maxLevel+1)
|
||
|
line.setEnabled(False)
|
||
|
else:
|
||
|
line.setMaximum(self.maxLevel)
|
||
|
line.setPageStep(1)
|
||
|
line.setSingleStep(1)
|
||
|
line.setOrientation(QtCore.Qt.Vertical)
|
||
|
line.setTickPosition(QtWidgets.QSlider.TicksAbove)
|
||
|
line.setTickInterval(1)
|
||
|
sliderCombo.addWidget(line)
|
||
|
line.hide()
|
||
|
|
||
|
#Stress-Level Below
|
||
|
numberLabel = QtWidgets.QLabel("0")
|
||
|
numberLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||
|
sliderCombo.addWidget(numberLabel)
|
||
|
numberLabel.hide()
|
||
|
|
||
|
line.valueChanged.connect(lambda v,l=numberLabel: l.setText(str(v)))
|
||
|
line.valueChanged.connect(self.convert)
|
||
|
self.positions.append((line, numberLabel, counterLabel))
|
||
|
|
||
|
self.positionLayout.addStretch()
|
||
|
|
||
|
#Signals and Init Values
|
||
|
self.howManyPositionsSpinBox.valueChanged.connect(self.setNumberOfPositions)
|
||
|
self.howManyPositionsSpinBox.setValue(4)
|
||
|
self.baseDuration.setValue(api.D4)
|
||
|
|
||
|
|
||
|
|
||
|
|
||
|
def convert(self):
|
||
|
"""Convert our linear representation into engine tree lists/tuplets.
|
||
|
|
||
|
There are some rules that make the task easier:
|
||
|
* The starting number is always one higher than the max user-choice. It exists only once.
|
||
|
* All groups containing zeroes are on the same level. This is not allowed: ((400) 20),
|
||
|
it must be ((400) (20))
|
||
|
"""
|
||
|
|
||
|
levels = set(line.value() for line, numberLabel, counterLabel in self.positions if line.isVisible())
|
||
|
levels.remove(self.maxLevel + 1)
|
||
|
|
||
|
result = ""
|
||
|
|
||
|
if not 0 in levels:
|
||
|
self.validState = False
|
||
|
levels = () #incorrect
|
||
|
else:
|
||
|
self.validState = True
|
||
|
highest = max(levels)
|
||
|
|
||
|
openGroupsStack = { l:False for l in levels } #True if a group is currently open
|
||
|
|
||
|
#for i in range(len(levels)):
|
||
|
|
||
|
result = []
|
||
|
ticks = self.baseDuration.value()
|
||
|
lastNonZeroValue = -1
|
||
|
for line, numberLabel, counterLabel in self.positions:
|
||
|
if not line.isVisible():
|
||
|
break
|
||
|
v = line.value()
|
||
|
|
||
|
if openGroupsStack[v]:
|
||
|
pass
|
||
|
else:
|
||
|
currentGroup = []
|
||
|
|
||
|
|
||
|
"""
|
||
|
if v == 0:
|
||
|
result += f"{ticks}, "
|
||
|
elif lastNonZeroValue < v:
|
||
|
result += f"({ticks}, "
|
||
|
lastNonZeroValue = v
|
||
|
elif lastNonZeroValue > v:
|
||
|
result += f"{ticks}), "
|
||
|
lastNonZeroValue = v
|
||
|
"""
|
||
|
|
||
|
#print (result)
|
||
|
self.parentWidget.sanityCheck()
|
||
|
|
||
|
return levels
|
||
|
|
||
|
|
||
|
|
||
|
def setNumberOfPositions(self, newValue:int):
|
||
|
"""Called by parent signal. Just hide and show"""
|
||
|
for line, stressLabel, counterLabel in self.positions:
|
||
|
line.hide()
|
||
|
stressLabel.hide()
|
||
|
counterLabel.hide()
|
||
|
|
||
|
for line, stressLabel, counterLabel in self.positions[:newValue]:
|
||
|
line.show()
|
||
|
stressLabel.show()
|
||
|
counterLabel.show()
|
||
|
stressLabel.setText(str(line.value()))
|
||
|
|
||
|
self.positions[0][0].paintEvent = nothing #"Hide" (but keep spacing) the slider for the first measure position. It is fixed to maxLevel + 1
|