Nils
3 years ago
3 changed files with 296 additions and 8 deletions
@ -0,0 +1,287 @@ |
|||
#! /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 |
Loading…
Reference in new issue