Browse Source

typehints

master
Nils 9 months ago
parent
commit
cf8a79180f
  1. 2
      template/engine/api.py
  2. 13
      template/engine/duration.py
  3. 2
      template/engine/input_midi.py
  4. 70
      template/engine/metronome.py
  5. 59
      template/engine/pitch.py
  6. 8
      template/engine/sequencer.py
  7. 13
      template/mypy.ini
  8. 87
      template/qtgui/about.py
  9. 2
      template/qtgui/helper.py
  10. 8
      template/qtgui/mainwindow.py
  11. 1
      template/qtgui/menu.py
  12. 2
      template/qtgui/nsmclient.py
  13. 22
      template/start.py

2
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.

13
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:

2
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

70
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,
}
}

59
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

8
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)

13
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

87
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 <a href='http://laborejo.org'>Laborejo Community</a>"),
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()

2
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

8
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

1
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):

2
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

22
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"""

Loading…
Cancel
Save