diff --git a/template/engine/api.py b/template/engine/api.py index 6c976d6..add010d 100644 --- a/template/engine/api.py +++ b/template/engine/api.py @@ -282,7 +282,7 @@ class Callbacks(object): func(channel) -def startEngine(nsmClient): +def startEngine(nsmClient, additionalData:dict={}): """ This function gets called after initializing the GUI, calfbox and loading saved data from a file. diff --git a/template/engine/duration.py b/template/engine/duration.py index f167ab2..f471916 100644 --- a/template/engine/duration.py +++ b/template/engine/duration.py @@ -26,9 +26,11 @@ This file handles various durations and their conversions. import logging; logger = logging.getLogger(__name__); logger.info("import") +from typing import Union from engine.config import METADATA + #Suggested import in other files: #from template.engine.duration import DB, DL, D1, D2, D4, D8, D16, D32, D64, D128 #import template.engine.duration as duration @@ -36,8 +38,7 @@ from engine.config import METADATA MAXIMUM_TICK_DURATION = 2**31-1 - -D4 = METADATA["quarterNoteInTicks"] +D4:int = METADATA["quarterNoteInTicks"] #type: ignore D8 = int(D4 / 2) D16 = int(D8 / 2) D32 = int(D16 / 2) @@ -69,7 +70,7 @@ D_STACCATO = 1 D_TENUTO = 2 D_TIE = 3 -def _baseDurationToTraditionalNumber(baseDuration): +def _baseDurationToTraditionalNumber(baseDuration:int)->int: """4 = Quarter, 8 = Eighth, 0 = Brevis, -1= Longa Created in the loop below on startup""" if baseDuration == D4: @@ -119,7 +120,7 @@ traditionalNumberToBaseDuration = {} for baseDuration in baseDurations: traditionalNumberToBaseDuration[_baseDurationToTraditionalNumber(baseDuration)] = baseDuration -def jackBBTicksToDuration(beatTypeAsTraditionalNumber, jackTicks, jackBeatTicks): +def jackBBTicksToDuration(beatTypeAsTraditionalNumber:int, jackTicks:int, jackBeatTicks:int)->Union[float, None]: if None in (beatTypeAsTraditionalNumber, jackTicks, jackBeatTicks): return None else: @@ -129,7 +130,7 @@ def jackBBTicksToDuration(beatTypeAsTraditionalNumber, jackTicks, jackBeatTicks) return jackTicks * factor -def lyDurationLogToTicks(durationLog): +def lyDurationLogToTicks(durationLog:int)->int: return 2**(8 - durationLog) * 6 ticksToLyDurationLogDict = { @@ -147,7 +148,7 @@ ticksToLyDurationLogDict = { D256: 8, #1/256 } -def ticksToLilypond(ticks): +def ticksToLilypond(ticks:int)->int: if ticks in ticksToLyDurationLogDict: return ticksToLyDurationLogDict[ticks] else: diff --git a/template/engine/input_midi.py b/template/engine/input_midi.py index 837e31c..0c8b917 100644 --- a/template/engine/input_midi.py +++ b/template/engine/input_midi.py @@ -257,7 +257,7 @@ class MidiProcessor(object): self.callbacks3[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)] = _printer else: try: - del self.callbacks[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)] + del self.callbacks2[(MidiProcessor.SIMPLE_EVENT, MidiProcessor.M_NOTE_ON)] except KeyError: pass diff --git a/template/engine/metronome.py b/template/engine/metronome.py index f795e1b..d3165e9 100644 --- a/template/engine/metronome.py +++ b/template/engine/metronome.py @@ -41,66 +41,66 @@ class Metronome(object): """ A metronome uses calfbox to generate a click track. All calfbox handling and midi generation are internally. - + The metronome has multiple components, each can be switched on and off on creation: - -stereo audio out (stereo for cbox reasons) - -midi out - -midi in - - You can configure the midi notes representing a stressed and normal tick. - There is no half-stressed signal. - + -stereo audio out (stereo for cbox reasons) + -midi out + -midi in + + You can configure the midi notes representing a stressed and normal tick. + There is no half-stressed signal. + Stressing can be switched off. - + The metronome is a real midi track, not generated on the fly. Therefore it needs to be set with new data when music changes, which happens quite often. In general you want you metronome to be as long as your song. - - You can choose to loop the last measure, which makes simple "give me 4/4 at 120" possible. - + + You can choose to loop the last measure, which makes simple "give me 4/4 at 120" possible. + """ def __init__(self, parentData, normalMidiNote=77, stressedMidiNote=76, midiChannel=9): self.parentData = parentData - #self.sequencerInterface = sequencer.SequencerInterface(parentTrack=self, name="metronome") + #self.sequencerInterface = sequencer.SequencerInterface(parentTrack=self, name="metronome") self.sfzInstrumentSequencerInterface = sequencer.SfzInstrumentSequencerInterface(parentTrack=self, name="metronome", absoluteSfzPath=os.path.join(PATHS["templateShare"], "metronome", "metronome.sfz")) #testing: self.sfzInstrumentSequencerInterface = sequencer.SequencerInterface(parentTrack=self, name="metronome"); self.sfzInstrumentSequencerInterface.setEnabled(True) needs activating below!!! - self._soundStresses = True #Change through soundStresses function + self._soundStresses = True #Change through soundStresses function self._normalTickMidiNote = normalMidiNote #no changing after instance got created self._stressedTickMidiNote = stressedMidiNote #no changing after instance got created self._midiChannel = midiChannel #GM drums #1-16 #no changing after instance got created self._cachedData = None #once we have a track it gets saved here so the midi output can be regenerated in place. self.label = "" #E.g. current Track Name, but can be anything. - self.setEnabled(False) #TODO: save load - - def soundStresses(self, value:bool): + self.setEnabled(False) #TODO: save load + + def soundStresses(self, value:bool): self._soundStresses = value - self.generate(self._cachedData) + self.generate(self._cachedData, self.label) def generate(self, data, label:str): """Data is ordered: Iterable of (positionInTicks, isMetrical, treeOfMetricalInstructions) as tuple. Does not check if we truly need an update - - Label typically is the track name""" - assert not data is None + + Label typically is the track name""" + assert not data is None self.label = label - self._cachedData = data - result = [] + self._cachedData = data + result = [] for position, isMetrical, treeOfMetricalInstructions in data: isMetrical = False if not self._soundStresses else True - blob, length = self.instructionToCboxMeasure(isMetrical, treeOfMetricalInstructions) - result.append((blob, position, length)) - self.sfzInstrumentSequencerInterface.setTrack(result) - #we skip over instructions which have no proto-measure, metrical or not. This basically creates a zone without a metronome. + blob, length = self.instructionToCboxMeasure(isMetrical, treeOfMetricalInstructions) + result.append((blob, position, length)) + self.sfzInstrumentSequencerInterface.setTrack(result) + #we skip over instructions which have no proto-measure, metrical or not. This basically creates a zone without a metronome. #This might be difficult to use when thinking of Laborejo alone but in combination with a real time audio recording in another program this becomes very useful. #TODO: repeat last instruction as loop - + @cache_unlimited def instructionToCboxMeasure(self, isMetrical, metricalInstruction:tuple)->Tuple[bytes,int]: """Convert a metrical instruction to a metronome measure""" measureBlob = bytes() workingTicks = 0 - ranOnce = False + ranOnce = False for duration in flatList(metricalInstruction): if ranOnce or not isMetrical: #normal tick. Always normal if this measure has no stressed positions, in other words it is not metrical measureBlob += cbox.Pattern.serialize_event(workingTicks, 0x90+9, 77, 127) #full velocity @@ -109,21 +109,21 @@ class Metronome(object): measureBlob += cbox.Pattern.serialize_event(workingTicks, 0x90+9, 76, 127) #different pitch measureBlob += cbox.Pattern.serialize_event(workingTicks+D1024, 0x80+9, 76, 127) ranOnce = True - workingTicks += duration + workingTicks += duration return (measureBlob, workingTicks) - + @property def enabled(self)->bool: - return self.sfzInstrumentSequencerInterface.enabled - + return self.sfzInstrumentSequencerInterface.enabled + def setEnabled(self, value:bool): self.sfzInstrumentSequencerInterface.enable(value) - def export(self)->dict: + def export(self)->dict: return { # "sequencerInterface" : self.sequencerInterface.export(), "enabled" : self.enabled, "label" : self.label, - } + } diff --git a/template/engine/pitch.py b/template/engine/pitch.py index defef08..4c3794b 100644 --- a/template/engine/pitch.py +++ b/template/engine/pitch.py @@ -28,11 +28,18 @@ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library from collections import defaultdict +from typing import Dict, Tuple, List, DefaultDict #Third Party Modules #Template Modules #Our modules +class KeySignature(object): + """For tests and typechecking. The real one is in Laborejo/engine/items.py""" + def __init__(self, root, deviationFromMajorScale): + self.root:int = root + self.deviationFromMajorScale:List = deviationFromMajorScale + self.keysigList:tuple = tuple() #Constants OCTAVE = 350 @@ -41,21 +48,21 @@ MAX = 3140 MIN = 0 #Without a keysignature -def plain(pitch): +def plain(pitch:int)->int: """ Extract the note from a note-number, without any octave but with the tailing zero. This means we double-use the lowest octave as abstract version.""" #Dividing through the octave, 350, results in the number of the octave and the note as remainder. return divmod(pitch, 350)[1] -def octave(pitch): +def octave(pitch:int)->int: """Return the octave of given note. Lowest 0 is X,,,""" return divmod(pitch, 350)[0] -def toOctave(pitch, octave): +def toOctave(pitch:int, octave:int)->int: """Take a plain note and give the octave variant. Starts with 0""" return pitch + octave * 350 -def mirror(pitch, axis): +def mirror(pitch:int, axis:int)->int: """Calculate the distance between the pitch and the axis-pitch and set the new pitch twice as far, which creates a mirror effect: Half the distancen is object->mirror and then mirror->object on the @@ -68,12 +75,12 @@ def mirror(pitch, axis): #1420 + 200 = 1620 return pitch + 2 * (axis - pitch) -def diatonicIndex(pitch): +def diatonicIndex(pitch:int)->int: """Return an int between 0 and 6, resembling the diatonic position of the given pitch without octave, accidentals. 0 is c""" return divmod(plain(toWhite[pitch]), 50)[0] -def absoluteDiatonicIndex(pitch): +def absoluteDiatonicIndex(pitch:int)->int: """Like diatonicIndex but works from pitch 20 which gets index 0 middle c is 28 tuning a is 33 @@ -81,13 +88,13 @@ def absoluteDiatonicIndex(pitch): (not traditional interval steps, real step counting from 1)""" return divmod(toWhite[pitch], 50)[0] -def distanceInDiatonicSteps(first, second): +def distanceInDiatonicSteps(first:int, second:int)->int: """root is a pitch like 1720. Pitch as well Returns not a signed int. If the first is lower than the second you get a negative return value.""" return absoluteDiatonicIndex(first) - absoluteDiatonicIndex(second) -def diatonicIndexToPitch(index, octave): +def diatonicIndexToPitch(index:int, octave:int)->int: """supports indices from - to +. """ while index < 0: index += 7 #plus one octave. index -1 becomes b @@ -97,7 +104,7 @@ def diatonicIndexToPitch(index, octave): octave += 1 return toOctave(index * 50 + 20, octave) #0 is cesces, 20 is c -def upStepsFromRoot(root, pitch): +def upStepsFromRoot(root:int, pitch:int)->int: """Like diatonicIndex but assumes a different root than C. So it is: 'stepcount upward in white keys from root to pitch'. It is always assumed it should go up. @@ -113,7 +120,7 @@ def upStepsFromRoot(root, pitch): p += 7 return p - r -def fromMidi(midipitch, keysig): +def fromMidi(midipitch:int, keysig:KeySignature)->int: """Convert a midi pitch to internal pitch. Nearest to pillar of fifth""" if (midipitch, keysig) in cache_fromMidi: @@ -162,7 +169,7 @@ def fromMidi(midipitch, keysig): -def halfToneDistanceFromC(pitch): +def halfToneDistanceFromC(pitch:int)->int: """Return the half-tone step distance from C. The "sounding" interval""" return { #00 : 10, # ceses,,, -> bes @@ -208,7 +215,7 @@ def halfToneDistanceFromC(pitch): }[plain(pitch)] -def sharpen(pitch): +def sharpen(pitch:int)->int: """Sharpen the pitch until double crossed""" sharper = pitch + 10 if toWhite[sharper] == toWhite[pitch]: #still the same base note? @@ -216,7 +223,7 @@ def sharpen(pitch): else: return pitch #too sharp, do nothing. -def flatten(pitch): +def flatten(pitch:int)->int: """Flatten the pitch until double flat""" flatter = pitch - 10 if toWhite[flatter] == toWhite[pitch]: #still the same base note? @@ -224,7 +231,7 @@ def flatten(pitch): else: return pitch #too flat, do nothing. -def interval(pitch1, pitch2): +def interval(pitch1:int, pitch2:int)->Tuple[int, int]: """Return the distance between two pitches as steps in the pillar of fifths. Intervals are tuplets with two members x = 1,0 #fifth in the same octave x[0] = interval. Steps in the pillar of fifths. @@ -235,7 +242,7 @@ def interval(pitch1, pitch2): else: return (pillarOfFifth.index(plain(pitch2)) - pillarOfFifth.index(plain(pitch1)), octave(pitch2 - pitch1)) -def intervalUp(pitch, interval, midiIn = False): +def intervalUp(pitch:int, interval:Tuple[int, int], midiIn:bool = False)->int: """Return a pitch which is _interval_ higher than the given pitch""" octv = octave(pitch) indexNumber = pillarOfFifth.index(plain(pitch)) @@ -245,7 +252,7 @@ def intervalUp(pitch, interval, midiIn = False): targetPitch += interval[1]*350 return targetPitch -def intervalDown(pitch, interval): +def intervalDown(pitch:int, interval:Tuple[int, int])->int: """Return a pitch which is _interval_ lower than the given pitch intervalUp(20, (-12,1)) #c,,, to deses,,""" @@ -257,18 +264,18 @@ def intervalDown(pitch, interval): targetPitch -= interval[1]*350 return targetPitch -def intervalAutomatic(originalPitch, rootPitch, targetPitch): +def intervalAutomatic(originalPitch:int, rootPitch:int, targetPitch:int)->int: """Return the original pitch transposed by the interval between rootPitch and targetPitch""" iv = interval(rootPitch, targetPitch) if rootPitch >= targetPitch: return intervalDown(originalPitch, iv) - elif rootPitch < targetPitch: + else: #rootPitch < targetPitch return intervalUp(originalPitch, iv) #With a Key Signature -def toScale(pitch, keysig): +def toScale(pitch:int, keysig:KeySignature)->int: """Return a pitch which is the in-scale variant of the given one. Needs a Key Signature as second parameter""" if (pitch, keysig) in cache_toScale: @@ -281,7 +288,7 @@ def toScale(pitch, keysig): cache_toScale[(pitch, keysig)] = value return value -def diffToKey(pitch, keysig): +def diffToKey(pitch:int, keysig:KeySignature)->int: """Return if a note is natural, sharp, flat etc. Same syntax as Key Signature: -20 double flat, 0 natural, +20 d-sharp.""" @@ -329,7 +336,7 @@ pillarOfFifth = [ ] -def midiPitchLimiter(pitch, transpose): +def midiPitchLimiter(pitch:int, transpose:int)->int: if pitch + transpose < 0: logger.warning(f"Tranpose lead to a note below midi value 0: {pitch}. Limiting to 0. Please fix manually") return 0 @@ -339,7 +346,7 @@ def midiPitchLimiter(pitch, transpose): else: return pitch + transpose -def midiChannelLimiter(value): +def midiChannelLimiter(value:int)->int: """makes sure that a midi channel is in range 0-15""" if value > 15: logger.warning("Midi Channel bigger 15 detected: {}. Limiting to 15. Please fix manually".format(value)) @@ -1057,8 +1064,8 @@ P_Bb = P_Hb = 270 - 10 #These are mostly values that are called multiple thousand times per track for every callback pitch2ly = dict((ly2pitch[k], k) for k in ly2pitch) -cache_toScale = {} #filled dynamically -cache_fromMidi = {} #filled dynamically +cache_toScale: Dict[ Tuple[int, KeySignature], int] = {} #filled dynamically. int is a pitch, object is a keysignature. value is pitch +cache_fromMidi: Dict[ Tuple[int, KeySignature], int] = {} #filled dynamically midipitch:keysig . type is (midipitch, keysig) : internal pitch toMidi = {} #filled for all pitches on startup, below toWhite = {} #filled for all pitches on startup, below tonalDistanceFromC = {} #filled for all pitches on startup, below @@ -1161,10 +1168,10 @@ midi_notenames_gm_drums = [str(i) for i in range(0,35)] + [ ] + [str(i) for i in range(87,128)] -def _defaultSimpleNoteNames(): +def _defaultSimpleNoteNames()->list: return midi_notenames_english -simpleNoteNames = defaultdict(_defaultSimpleNoteNames) +simpleNoteNames:DefaultDict[str,list] = defaultdict(_defaultSimpleNoteNames) simpleNoteNames["German"] = midi_notenames_german simpleNoteNames["English"] = midi_notenames_english simpleNoteNames["Lilypond"] = midi_notenames_lilypond diff --git a/template/engine/sequencer.py b/template/engine/sequencer.py index a68af4f..331ba24 100644 --- a/template/engine/sequencer.py +++ b/template/engine/sequencer.py @@ -23,7 +23,7 @@ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Library -from typing import List, Dict, Tuple, Iterable +from typing import List, Dict, Tuple, Iterable, Union #Third Party Modules from calfbox import cbox @@ -90,7 +90,7 @@ class Score(Data): #Tracks def addTrack(self, name:str=""): """Create and add a new track. Not an existing one""" - track = Score.TrackClass(parentData=self, name=name) + track = Score.TrackClass(parentData=self, name=name) # type: ignore # mypy doesn't know that this is an injected class variable assert track.sequencerInterface self.tracks.append(track) return track @@ -361,7 +361,7 @@ class SequencerInterface(_Interface): #Basically the midi part of a track. self._enabled = bool(enabled) cbox.Document.get_song().update_playback() - @_Interface.name.setter + @_Interface.name.setter # type: ignore def name(self, value): if not value in (track.sequencerInterface.name for track in self.parentData.tracks): self._name = self._isNameAvailable(value) @@ -667,7 +667,7 @@ class TempoMap(object): currentValue, OLD_timesigNum, OLD_timesigDenom = self._tempoMap[0] self.setTempoMap({0:(currentValue, timesigNum, timesigDenom)}) - def getQuarterNotesPerMinute(self)->float: + def getQuarterNotesPerMinute(self)->Union[float, None]: """This assumes there is only one tempo point""" if self.isTransportMaster: assert len(self._tempoMap) == 1, len(self._tempoMap) diff --git a/template/mypy.ini b/template/mypy.ini new file mode 100644 index 0000000..31a6917 --- /dev/null +++ b/template/mypy.ini @@ -0,0 +1,13 @@ +[mypy] + +[mypy-*.calfbox.*] +ignore_missing_imports = True + +[mypy-calfbox.*] +ignore_missing_imports = True + +[mypy-compiledprefix.*] +ignore_missing_imports = True + +[mypy-*.designer.*] +ignore_missing_imports = True diff --git a/template/qtgui/about.py b/template/qtgui/about.py index b37931e..dce47c1 100644 --- a/template/qtgui/about.py +++ b/template/qtgui/about.py @@ -23,6 +23,7 @@ import logging; logger = logging.getLogger(__name__); logger.info("import") #Standard Lib from random import choice +from typing import List #System Wide Modules from PyQt5 import QtCore, QtWidgets, QtGui @@ -40,27 +41,27 @@ import engine.api as api #Already loaded, will not change anything class About(QtWidgets.QDialog): """A help window with useful tips. Also the nagscreen for Donations. - + The didYouKnow sentences stay here to be edited and not in engine/constants.py? - They are GUI only and need to be translated - + They are GUI only and need to be translated + The About dialog depends on a set key guiSharedDataToSave["showAboutDialog"]. That is usually - created in the mainWindow for a new instance, never saved, and intial value depends on + created in the mainWindow for a new instance, never saved, and intial value depends on METADATA["showAboutDialogFirstStart"] """ - didYouKnow = [] #Will be filled by the non-template part of the program + didYouKnow:List[str] = [] #Will be filled by the non-template part of the program def __init__(self, mainWindow): #super(DidYouKnow, self).__init__(parent, QtCore.Qt.WindowStaysOnTopHint|QtCore.Qt.CustomizeWindowHint|QtCore.Qt.X11BypassWindowManagerHint) #super(DidYouKnow, self).__init__(parent, QtCore.Qt.FramelessWindowHint) - - super().__init__(parent=mainWindow) + + super().__init__(parent=mainWindow) self.setModal(True) #block until closed - self.ui = Ui_TemplateAbout() + self.ui = Ui_TemplateAbout() self.ui.setupUi(self) self.mainWindow = mainWindow - + self.ui.didyouknowLabel.setText(choice(self.tricks())) self.index = self.tricks().index(self.ui.didyouknowLabel.text()) @@ -70,27 +71,27 @@ class About(QtWidgets.QDialog): self.ui.numberLabel.setText("Tip " + str(self.index+1) + "/" + str(len(self.tricks()))) self.ui.numberSlider.valueChanged.connect(self.moveSlider) - + copyright = f"""{METADATA["author"]} ({METADATA["year"]})""" name = f"""{METADATA["name"]} ver. {METADATA["version"]}""" aboutText = "\n".join([name, copyright, METADATA["url"]]) self.ui.aboutLabel.setText(aboutText) - #Image: 300x151 + #Image: 300x151 aboutLogoPixmap = QtGui.QPixmap(":aboutlogo.png") #pixmap_scaled = aboutLogoPixmap.scaled(self.ui.goldenratioLabel.size(), QtCore.Qt.KeepAspectRatio) #self.ui.goldenratioLabel.setPixmap(pixmap_scaled) - self.ui.goldenratioLabel.setPixmap(aboutLogoPixmap) + self.ui.goldenratioLabel.setPixmap(aboutLogoPixmap) """ - #We don't want the user to get bombarded with information on the first start. + #We don't want the user to get bombarded with information on the first start. #Only show us the second time. - if not "showAboutDialog" in api.session.guiSharedDataToSave: - self.ui.showOnStartup.hide() - api.session.guiSharedDataToSave["showAboutDialog"] = True - """ - - self.ui.showOnStartup.stateChanged.connect(self.saveStartupState) + if not "showAboutDialog" in api.session.guiSharedDataToSave: + self.ui.showOnStartup.hide() + api.session.guiSharedDataToSave["showAboutDialog"] = True + """ + + self.ui.showOnStartup.stateChanged.connect(self.saveStartupState) """ QAbstractSlider.SliderNoAction 0 @@ -110,48 +111,48 @@ class About(QtWidgets.QDialog): self.ui.numberSlider.wheelEvent = self.mouseWheelEventCustom #Deactivate the normal number slider wheel event self.wheelEvent = self.mouseWheelEventCustom #Use a window wide one that is easier to control - - settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) self.ui.showOnStartup.setChecked(settings.value("showAboutDialog", type=bool)) self.ui.numberSlider.setFocus(True) - + def tricks(self): """For some reason translations do not work if saved as class variable. - A getter function works""" - + A getter function works""" + return About.didYouKnow + [ #Make the first three words matter! #Do not start them all with "You can..." or "...that you can", in response to the Did you know? title. QtCore.QCoreApplication.translate("TemplateAbout", "Help can be found through the Laborejo Community"), - QtCore.QCoreApplication.translate("TemplateAbout", "Temporary Shortcuts can be created by hovering the mouse cursor over a menu entry (with no shortcut) and pressing a numpad key"), - QtCore.QCoreApplication.translate("TemplateAbout", "Closing the program with the [X] icon or shortcuts like Alt+F4 will only hide the window. This state will be saved, so you don't need to see the window each time you load your session."), - ] - - def reject(self): + QtCore.QCoreApplication.translate("TemplateAbout", "Temporary Shortcuts can be created by hovering the mouse cursor over a menu entry (with no shortcut) and pressing a numpad key"), + QtCore.QCoreApplication.translate("TemplateAbout", "Closing the program with the [X] icon or shortcuts like Alt+F4 will only hide the window. This state will be saved, so you don't need to see the window each time you load your session."), + ] + + def reject(self): self.hide() - + def showEvent(self, event): """The qt main loop is slow. We can't show the about dialog, or any other sub-dialog, right after mainWindow.show() becauer the event loop is not ready and the screen positions will be wrong. - - Instead we wait that the showEvent actually arrives. + + Instead we wait that the showEvent actually arrives. And even then we wait a few ms, to be on the safe side. That was actually needed on the devs system. - + Also the dialog is sometimes not closed but hidden. - Also we had reports of not correctly parented and centered dialogs. - - Make sure everything is where it should be""" + Also we had reports of not correctly parented and centered dialogs. + + Make sure everything is where it should be""" parentCenter = self.mainWindow.geometry().center() - aboutCenter = self.geometry().center() + aboutCenter = self.geometry().center() self.move(parentCenter - aboutCenter) event.accept() - - def mouseWheelEventCustom(self, event): - event.accept() + + def mouseWheelEventCustom(self, event): + event.accept() if event.angleDelta().y() > 0: self.ui.numberSlider.triggerAction(1) else: @@ -163,9 +164,9 @@ class About(QtWidgets.QDialog): act.triggered.connect(function) self.addAction(act) - def saveStartupState(self): - settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) - settings.setValue("showAboutDialog", bool(self.ui.showOnStartup.checkState())) + def saveStartupState(self): + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + settings.setValue("showAboutDialog", bool(self.ui.showOnStartup.checkState())) def moveSlider(self): nowIdx = self.ui.numberSlider.value() diff --git a/template/qtgui/helper.py b/template/qtgui/helper.py index 8ec359e..70a06fe 100644 --- a/template/qtgui/helper.py +++ b/template/qtgui/helper.py @@ -253,7 +253,7 @@ class ToggleSwitch(QtWidgets.QAbstractButton): } self._track_opacity = 0.7 - @QtCore.pyqtProperty(int) + @property def offset(self): return self._offset diff --git a/template/qtgui/mainwindow.py b/template/qtgui/mainwindow.py index 9dafbb2..e5ade66 100644 --- a/template/qtgui/mainwindow.py +++ b/template/qtgui/mainwindow.py @@ -61,13 +61,13 @@ api.session.eventLoop = EventLoop() #Language variants like de_AT.UTF-8 will be detected automatically and will result in Qt language detection as "German" language = QtCore.QLocale().languageToString(QtCore.QLocale().language()) logger.info("{}: Language set to {}".format(METADATA["name"], language)) -if language in METADATA["supportedLanguages"]: +if language in METADATA["supportedLanguages"]: # type: ignore templateTranslator = QtCore.QTranslator() - templateTranslator.load(METADATA["supportedLanguages"][language], ":/template/translations/") #colon to make it a resource URL + templateTranslator.load(METADATA["supportedLanguages"][language], ":/template/translations/") # type: ignore #colon to make it a resource URL qtApp.installTranslator(templateTranslator) otherTranslator = QtCore.QTranslator() - otherTranslator.load(METADATA["supportedLanguages"][language], ":translations") #colon to make it a resource URL + otherTranslator.load(METADATA["supportedLanguages"][language], ":translations") # type: ignore #colon to make it a resource URL qtApp.installTranslator(otherTranslator) else: @@ -148,7 +148,7 @@ class MainWindow(QtWidgets.QMainWindow): if api.session.guiWasSavedAsNSMVisible: self.showGUI() - settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) + settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"]) # type: ignore #mypy cannot handle METADATA if settings.contains("showAboutDialog") and settings.value("showAboutDialog", type=bool): QtCore.QTimer.singleShot(100, self.about.show) #Qt Event loop is not ready at that point. We need to wait for the paint event. This is not to stall for time: Using the event loop guarantees that it exists elif not self.nsmClient.sessionName == "NOT-A-SESSION": #standalone mode diff --git a/template/qtgui/menu.py b/template/qtgui/menu.py index 3a8db0e..9aa0963 100644 --- a/template/qtgui/menu.py +++ b/template/qtgui/menu.py @@ -211,7 +211,6 @@ class Menu(object): menuAction.setVisible(False) def removeSubmenu(self, submenuAction:str): - menuAction = getattr(self.ui, submenu).menuAction() raise NotImplementedError #TODO def addMenuEntry(self, submenu, actionAsString:str, text:str, connectedFunction=None, shortcut:str="", tooltip:str="", iconResource:str="", checkable=False, startChecked=False): diff --git a/template/qtgui/nsmclient.py b/template/qtgui/nsmclient.py index fac7cad..ef4ebc1 100644 --- a/template/qtgui/nsmclient.py +++ b/template/qtgui/nsmclient.py @@ -28,7 +28,7 @@ OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWA """ import logging; -logger = None #filled by init with prettyName +logger: logging.Logger #filled by init with client logger. import struct import socket diff --git a/template/start.py b/template/start.py index 7a3a7c1..2fe80f2 100644 --- a/template/start.py +++ b/template/start.py @@ -32,8 +32,7 @@ Same with the tests if jack or nsm are running. #But every bit helps when hunting bugs. import faulthandler; faulthandler.enable() - -from engine.config import * #includes METADATA only. No other environmental setup is executed. +from engine.config import METADATA #includes METADATA only. No other environmental setup is executed. from template.qtgui.chooseSessionDirectory import ChooseSessionDirectory from template.qtgui.helper import setPaletteAndFont #our error boxes shall look like the rest of the program @@ -93,7 +92,8 @@ logger.info(f"Script dir: {get_script_dir()}") logger.info(f"Python Version {sys.version}") try: - from compiledprefix import prefix + from compiledprefix import prefix as prefix_import + prefix:str = prefix_import compiledVersion = True logger.info("Compiled prefix found: {}".format(prefix)) except ModuleNotFoundError as e: @@ -101,22 +101,22 @@ except ModuleNotFoundError as e: logger.info("Compiled version: {}".format(compiledVersion)) -cboxSharedObjectVersionedName = "lib"+METADATA["shortName"]+".so." + METADATA["version"] +cboxSharedObjectVersionedName = "lib"+METADATA["shortName"]+".so." + METADATA["version"] # type: ignore logger.info("Our calfbox extension cpython library should be named " + cboxSharedObjectVersionedName) #ZippApp with compiledprefix.py if compiledVersion: PATHS={ #this gets imported "root": "", - "bin": os.path.join(prefix, "bin"), - "doc": os.path.join(prefix, "share", "doc", METADATA["shortName"]), - "desktopfile": os.path.join(prefix, "share", "applications", METADATA["shortName"] + ".desktop"), #not ~/Desktop but our desktop file - "share": os.path.join(prefix, "share", METADATA["shortName"]), - "templateShare": os.path.join(prefix, "share", METADATA["shortName"], "template"), + "bin": os.path.join(prefix, "bin"), # type: ignore + "doc": os.path.join(prefix, "share", "doc", METADATA["shortName"]), # type: ignore + "desktopfile": os.path.join(prefix, "share", "applications", METADATA["shortName"] + ".desktop"), # type: ignore #not ~/Desktop but our desktop file + "share": os.path.join(prefix, "share", METADATA["shortName"]), # type: ignore + "templateShare": os.path.join(prefix, "share", METADATA["shortName"], "template"), # type: ignore #"lib": os.path.join(prefix, "lib", METADATA["shortName"]), #cbox is found via the PYTHONPATH } - cboxSharedObjectPath = os.path.join(prefix, "lib", METADATA["shortName"], cboxSharedObjectVersionedName) + cboxSharedObjectPath = os.path.join(prefix, "lib", METADATA["shortName"], cboxSharedObjectVersionedName) # type: ignore _root = os.path.dirname(__file__) _root = os.path.abspath(os.path.join(_root, "..")) @@ -301,7 +301,7 @@ def profiler(*pargs, **kwds): #Catch Exceptions even if PyQt crashes. import sys -sys._excepthook = sys.excepthook +sys._excepthook = sys.excepthook # type: ignore #mypy thinks that doesn't exist, eventhough we set it ourselves right here. def exception_hook(exctype, value, traceback): """This hook purely exists to call sys.exit(1) even on a Qt crash so that atexit gets triggered"""