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