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.
 
 

287 lines
10 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")
#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