#! /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 ) ,
This application 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 " )
#Standard Library
#Third Party
from PyQt5 import QtCore , QtGui , QtWidgets
#Our Qt
#Engine
import engine . api as api
class SelectedInstrumentController ( object ) :
""" Not a qt class. We externally control a collection of widgets.
There is only one set of widgets . We change their contents dynamically .
"""
def __init__ ( self , parentMainWindow ) :
self . parentMainWindow = parentMainWindow
self . currentIdKey = None
self . engineData = { } # idKey tuple : engine library metadata dict.
self . statusUpdates = { } # same as engineData, but with incremental status updates. One is guranteed to exist at startup
#Our Widgets
self . ui = parentMainWindow . ui
self . ui . details_groupBox . setTitle ( " " )
self . ui . details_scrollArea . hide ( ) #until the first instrument was selected
self . ui . variants_comboBox . activated . connect ( self . _newVariantChosen )
self . ui . keySwitch_comboBox . activated . connect ( self . _newKeySwitchChosen )
#Callbacks
api . callbacks . instrumentListMetadata . append ( self . react_initialInstrumentList )
api . callbacks . instrumentStatusChanged . append ( self . react_instrumentStatusChanged )
def directLibrary ( self , idkey : tuple ) :
""" User clicked on a library treeItem """
libraryId , instrumentId = idkey
self . currentIdKey = None
self . ui . details_scrollArea . show ( )
self . ui . variants_comboBox . hide ( )
self . ui . variant_label . hide ( )
self . ui . keySwitch_label . hide ( )
self . ui . keySwitch_comboBox . hide ( )
metadata = self . engineData [ libraryId ] [ " library " ]
self . ui . details_groupBox . setTitle ( metadata [ " name " ] )
self . ui . info_label . setText ( self . _metadataToDescriptionLabel ( metadata ) )
def _metadataToDescriptionLabel ( self , metadata : dict ) - > str :
""" Can work with instruments and libraries alike """
if " variants " in metadata : #this is an instrument
fullText = metadata [ " description " ] + " \n \n Vendor: " + metadata [ " vendor " ] + " \n \n License: " + metadata [ " license " ]
else :
fullText = metadata [ " description " ] + " \n \n Vendor: " + metadata [ " vendor " ]
return fullText
def _newVariantChosen ( self , index : int ) :
""" User chose a new variant through the combo box """
assert self . ui . variants_comboBox . currentIndex ( ) == index
assert self . currentIdKey
api . chooseVariantByIndex ( self . currentIdKey , index )
def _newKeySwitchChosen ( self , index : int ) :
""" User chose a new keyswitch through the combo box.
Answer comes back via react_instrumentStatusChanged """
assert self . ui . keySwitch_comboBox . currentIndex ( ) == index
assert self . currentIdKey
#Send back the midi pitch, not the index
api . setInstrumentKeySwitch ( self . currentIdKey , self . ui . keySwitch_comboBox . itemData ( index ) )
def _populateKeySwitchComboBox ( self , instrumentStatus ) :
""" Convert engine format from callback to qt combobox string.
This is called when activating an instrument , switching the variant or simply
coming back to an already loaded instrument in the GUI .
This might come in from a midi callback when we are not currently selected .
We test if this message is really for us .
"""
#TODO: distinguish between momentary keyswitches sw_up sw_down and permanent sw_last. We only want sw_last as selection but the rest must be shown as info somewhere.
self . ui . keySwitch_comboBox . clear ( )
if not instrumentStatus or not " keySwitches " in instrumentStatus : #not all instruments have keyswitches
self . ui . keySwitch_comboBox . setEnabled ( False )
return
engineKeySwitchDict = instrumentStatus [ " keySwitches " ]
self . ui . keySwitch_comboBox . setEnabled ( True )
for midiPitch , ( opcode , label ) in sorted ( engineKeySwitchDict . items ( ) ) :
self . ui . keySwitch_comboBox . addItem ( f " [ { midiPitch } ]: { label } " , userData = midiPitch )
#set current one, if any. If not the engine-dict is None and we set to an empty entry, even if there are keyswitches. which is fine.
curIdx = self . ui . keySwitch_comboBox . findData ( instrumentStatus [ " currentKeySwitch " ] )
self . ui . keySwitch_comboBox . setCurrentIndex ( curIdx )
def instrumentChanged ( self , idkey : tuple ) :
""" This is a GUI-internal function. The user selected a different instrument from
the list . Single click , arrow keys etc .
We combine static metadata , which we saved ourselves , with the current instrument status
( e . g . which variant was chosen ) .
"""
libraryId , instrumentId = idkey
self . currentIdKey = idkey
self . ui . details_scrollArea . show ( )
#Cached
instrumentStatus = self . statusUpdates [ libraryId ] [ instrumentId ]
#Static
instrumentData = self . engineData [ libraryId ] [ instrumentId ]
self . ui . details_groupBox . setTitle ( instrumentData [ " name " ] )
self . ui . keySwitch_label . show ( )
self . ui . keySwitch_comboBox . show ( )
self . _populateKeySwitchComboBox ( instrumentStatus [ " keySwitches " ] ) #clears
self . ui . variant_label . show ( )
self . ui . variants_comboBox . show ( )
self . ui . variants_comboBox . clear ( )
self . ui . variants_comboBox . addItems ( instrumentData [ " variantsWithoutSfzExtension " ] )
self . ui . info_label . setText ( self . _metadataToDescriptionLabel ( instrumentData ) )
#Dynamic
self . react_instrumentStatusChanged ( self . statusUpdates [ libraryId ] [ instrumentId ] )
def react_instrumentStatusChanged ( self , instrumentStatus : dict ) :
""" Callback from the api. Has nothing to do with any GUI state or selection.
Happens if the user loads a variant .
Data :
#Static ids
result [ " id " ] = self . metadata [ " id " ]
result [ " idKey " ] = self . idKey #redundancy for convenience.
#Dynamic data
result [ " currentVariant " ] = self . currentVariant # str
result [ " state " ] = self . enabled #bool
It is possible that this callback comes in from a midi trigger , so we have no guarantee
that this matches the currently selected instrument in the GUI . We will cache the updated
status but check if this is our message before changing any GUI fields .
This callback is called again by the GUI directly when switching the instrument with a
mouseclick in instrumentChanged and the GUI will use the cached data .
"""
idkey = instrumentStatus [ " idKey " ]
libraryId , instrumentId = idkey
if not libraryId in self . statusUpdates :
self . statusUpdates [ libraryId ] = { } #empty library. status dict
self . statusUpdates [ libraryId ] [ instrumentId ] = instrumentStatus #create or overwrite / keep up to date
if not self . currentIdKey == idkey :
#Callback for an instrument currently not selected
return
loadState = instrumentStatus [ " state " ]
instrumentData = self . engineData [ libraryId ] [ instrumentId ]
instrumentStatus = self . statusUpdates [ libraryId ] [ instrumentId ]
if loadState : #None if not loaded
self . ui . variants_comboBox . setEnabled ( True )
currentVariantIndex = instrumentData [ " variantsWithoutSfzExtension " ] . index ( instrumentStatus [ " currentVariantWithoutSfzExtension " ] )
self . ui . variants_comboBox . setCurrentIndex ( currentVariantIndex )
else :
self . ui . variants_comboBox . setEnabled ( False )
defaultVariantIndex = instrumentData [ " variantsWithoutSfzExtension " ] . index ( instrumentData [ " defaultVariantWithoutSfzExtension " ] )
self . ui . variants_comboBox . setCurrentIndex ( defaultVariantIndex )
self . _populateKeySwitchComboBox ( instrumentStatus )
def react_initialInstrumentList ( self , data : dict ) :
""" For data form see docstring of instrument.py buildTree()
Summary :
libraryid : dict - >
instrumentid : metadatadict
Dict - Keys are always the same . Some always have data , some can be empty .
We receive this once at program start and build our permanent GUI widgets from it .
The additional status update callback for dynamic data is handled in
self . react_instrumentStatusChanged
"""
self . engineData = data