#! /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 #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): """Init is called everytime the dialog gets shown""" super().__init__(mainWindow) self.mainWindow = mainWindow self.ui = Ui_customKeySignature() self.ui.setupUi(self) 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) api.insertKeySignature(root=selectedRootPitch, scheme=deviationFromMajorScale) self.done(True)