#! /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 . """ 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