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.
 
 

258 lines
9.0 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
#Template Modules
from template.engine.pitch import baseNotesToAccidentalNames, plain, intervalAutomatic, diatonicIndex
#Our Modules
from .designer.customKeySignature import Ui_customKeySignature
import engine.api as api
class CustomKeySignatureWidget(QtWidgets.QDialog):
def __init__(self, mainWindow, setInitialKeysigInsteadCursorInsert:bool=False):
"""Init is called everytime the dialog gets shown.
This function can be used to insert a normal sig at cursor position, which is the main
purpose.
But later we retrofitted the option to set the initial keysig for all tracks,
which is just a different api function.
set setInitialKeysigInsteadCursorInsert to True for this behaviour.
"""
super().__init__(mainWindow)
self.mainWindow = mainWindow
self.ui = Ui_customKeySignature()
self.ui.setupUi(self)
self.setInitialKeysigInsteadCursorInsert = setInitialKeysigInsteadCursorInsert
self.ui.buttonBox.accepted.connect(self.process)
self.ui.buttonBox.rejected.connect(self.reject)
currentCursorPitch = api.getCursorPlainNote()
self.resultLabels = (
self.ui.label_result_c,
self.ui.label_result_d,
self.ui.label_result_e,
self.ui.label_result_f,
self.ui.label_result_g,
self.ui.label_result_a,
self.ui.label_result_b,
)
self.rootNotes = {
20 : self.ui.root_c ,
70 : self.ui.root_d ,
120 : self.ui.root_e ,
170 : self.ui.root_f ,
220 : self.ui.root_g ,
270 : self.ui.root_a ,
320 : self.ui.root_b ,
10 : self.ui.root_ces ,
60 : self.ui.root_des ,
110 : self.ui.root_ees ,
160 : self.ui.root_fes ,
210 : self.ui.root_ges ,
260 : self.ui.root_aes ,
310 : self.ui.root_bes ,
30 : self.ui.root_cis ,
80: self.ui.root_dis ,
130 : self.ui.root_eis ,
180: self.ui.root_fis ,
230 : self.ui.root_gis ,
280: self.ui.root_ais ,
330 : self.ui.root_bis ,
}
#Add reverse lookup
self.buttonToRootNote = {}
for key, value in self.rootNotes.items():
self.buttonToRootNote[value] = key
self.rootNotes[currentCursorPitch].setChecked(True)
for button in self.rootNotes.values():
button.clicked.connect(self.sanityCheck)
### Scale Radio Buttons.
### Please note that the names are just shorthands because we create a relative scale here.
### Those really should be roman numerals for first note, second note, third etc. but this is cumbersome to write
self.scaleButtons = (
(
self.ui.isis_c ,
self.ui.is_c ,
self.ui.nat_c ,
self.ui.es_c ,
self.ui.eses_c ,
),
(
self.ui.isis_d ,
self.ui.is_d ,
self.ui.nat_d ,
self.ui.es_d ,
self.ui.eses_d ,
),
(
self.ui.isis_e ,
self.ui.is_e ,
self.ui.nat_e ,
self.ui.es_e ,
self.ui.eses_e ,
),
(
self.ui.isis_f ,
self.ui.is_f ,
self.ui.nat_f ,
self.ui.es_f ,
self.ui.eses_f ,
),
(
self.ui.isis_g ,
self.ui.is_g ,
self.ui.nat_g ,
self.ui.es_g ,
self.ui.eses_g ,
),
(
self.ui.isis_a ,
self.ui.is_a ,
self.ui.nat_a ,
self.ui.es_a ,
self.ui.eses_a ,
),
(
self.ui.isis_b ,
self.ui.is_b ,
self.ui.nat_b ,
self.ui.es_b ,
self.ui.eses_b ,
),
)
for group in self.scaleButtons:
group[2].setChecked(True) #Make natural the default.
for button in group:
button.setStyleSheet("QRadioButton { font : 18px }")
button.clicked.connect(self.sanityCheck)
self.exec() #blocks until the dialog gets closed
def blockSignals(self, state:bool):
for group in self.scaleButtons:
for button in group:
button.blockSignals(state)
for button in self.rootNotes.values():
button.blockSignals(state)
def findRootPitch(self)->int:
for rootButton, basePitch in self.buttonToRootNote.items():
if rootButton.isChecked():
return basePitch
else:
raise RuntimeError()
def groupToInScalePitch(self, groupIndex:int, checkSpecificStep:int=None):
selectedRootPitch = self.findRootPitch()
stepInCMajor = (groupIndex+1)*50 - 30
#The transposed interval scale is also major, of course.
stepInMajorRootScale = intervalAutomatic(originalPitch=stepInCMajor, rootPitch=20, targetPitch=selectedRootPitch)
if checkSpecificStep is None:
#find the accidental mod
group = self.scaleButtons[groupIndex]
for counter, button in enumerate(reversed(group)): #group is isis to eses, so we need to reverse.
if button.isChecked():
#counter remains
break
else:
counter = checkSpecificStep
mod = 10*counter-20 #offset by -20 because the scale starts at -20 for double flat but we enumerate from 0
plainPitch = plain(stepInMajorRootScale + mod)
pitchOk = diatonicIndex(stepInMajorRootScale) == diatonicIndex(plainPitch) #did we cross a diatonic note boundary? That means this was a triple accidental
return plainPitch, pitchOk
def sanityCheck(self, buttonChecked:bool):
"""Called after every change to the whole widget.
Recalculates note names and checks if any of the steps result in triple accidentals.
We choose signal:clicked because signal:toggled triggers this signal twice because
for each radio toggle another radio button get's deactivated automatically."""
for groupIndex, group in enumerate(self.scaleButtons):
#Sanity check. Check for triple accidentals
for counter, button in enumerate(reversed(group)):
plainPitch, pitchOk = self.groupToInScalePitch(groupIndex, counter)
if pitchOk:
button.setEnabled(True)
else:
button.setEnabled(False)
if button.isChecked():
button.setChecked(False) #setChecked does NOT activate the clicked signal. We are safe from recursive calls.
group[2].setChecked(True) #Activate natural as fallback
#Calculate Labels
plainPitch, pitchOk = self.groupToInScalePitch(groupIndex)
assert pitchOk
self.resultLabels[groupIndex].setText(baseNotesToAccidentalNames[plainPitch])
def process(self):
selectedRootPitch = self.findRootPitch()
#Gather selected scale into engine scale list, which is the deviation from the major scale.
#THIS IS A RELATIVE SCALE, not accidentals for notes. The GUI shows roman numerals for scale building, not note names.
#-20 is double flat, -10 is flat, 0 is natural, 10 is sharp, 20 is double sharp
#self.scaleButtons is semi-correctly ordered: ascending scale (outer list) but from double sharp to double flat (inner list). we reverse the latter
deviationFromMajorScale = []
for group in self.scaleButtons:
for counter, button in enumerate(reversed(group)):
if button.isChecked():
deviationFromMajorScale.append(10*counter -20)
if self.setInitialKeysigInsteadCursorInsert:
api.setInitialKeySignatures(root=selectedRootPitch, scheme=deviationFromMajorScale)
else: #default
api.insertKeySignature(root=selectedRootPitch, scheme=deviationFromMajorScale)
self.done(True)