#! /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
from template . qtgui . helper import ToggleSwitch , FancySwitch
from template . qtgui . submenus import nestedJackPortsMenu
#Engine
import engine . api as api
COLUMNS = ( " state " , " idKey " , " mixSend " , " name " , " loaded " , " group " , " tags " ) #Loaded = Variant
class InstrumentTreeController ( object ) :
"""
Shows the list of instruments , so they can be clicked upon : )
Not a qt class . We externally controls the QTreeWidget
Why is this not a QTableWidget ? As in Agordejo , a TableWidget is a complex item , and inconvenient
to use . You need to add an Item to each cell . While in TreeWidget you just create one item .
"""
def __init__ ( self , parentMainWindow , treeWidget ) :
self . parentMainWindow = parentMainWindow
self . treeWidget = treeWidget
self . reset ( )
#Includes:
#self._cachedData = None
#self._cachedLastInstrumentStatus = {} # instrument idKey : status Dict
#self.guiLibraries = {} # idKey : GuiLibrary. idKey is a tuple with second value -1, which would be the instrument.
#self.guiInstruments = {} # idKey : GuiInstrument
self . headerLabels = [
QtCore . QCoreApplication . translate ( " InstrumentTreeController " , " Enable " ) ,
QtCore . QCoreApplication . translate ( " InstrumentTreeController " , " ID " ) ,
QtCore . QCoreApplication . translate ( " InstrumentTreeController " , " toMix " ) ,
QtCore . QCoreApplication . translate ( " InstrumentTreeController " , " Name " ) ,
QtCore . QCoreApplication . translate ( " InstrumentTreeController " , " Loaded " ) ,
QtCore . QCoreApplication . translate ( " InstrumentTreeController " , " Group " ) ,
QtCore . QCoreApplication . translate ( " InstrumentTreeController " , " Tags " ) ,
]
self . treeWidget . setColumnCount ( len ( self . headerLabels ) )
self . treeWidget . setHeaderLabels ( self . headerLabels )
self . treeWidget . setSortingEnabled ( True )
self . treeWidget . setSelectionMode ( QtWidgets . QAbstractItemView . SingleSelection )
self . treeWidget . setSelectionBehavior ( QtWidgets . QAbstractItemView . SelectRows )
self . treeWidget . setAlternatingRowColors ( True )
self . treeWidget . setContextMenuPolicy ( QtCore . Qt . CustomContextMenu )
self . treeWidget . header ( ) . setSortIndicator ( 0 , 0 )
self . treeWidget . itemDoubleClicked . connect ( self . itemDoubleClicked )
self . treeWidget . currentItemChanged . connect ( self . parentMainWindow . currentTreeItemChanged )
#self.treeWidget.itemSelectionChanged.connect(self.itemSelectionChanged) #also triggers on tab change between favorits and instruments
#self.treeWidget.itemClicked.connect(self.itemSelectionChanged) #This will not activate when using the arrow keys to select
self . treeWidget . itemExpanded . connect ( self . itemExpandedOrCollapsed )
self . treeWidget . itemCollapsed . connect ( self . itemExpandedOrCollapsed )
self . treeWidget . customContextMenuRequested . connect ( self . contextMenu )
self . sortByColumnValue = 1 #by instrId
self . sortDescendingValue = 0 # Qt::SortOrder which is 0 for ascending and 1 for descending
api . callbacks . instrumentListMetadata . append ( self . buildTree ) #called without arguments it will just create the standard tree. This will only happen once at program start.
api . callbacks . startLoadingSamples . append ( self . react_startLoadingSamples )
api . callbacks . instrumentStatusChanged . append ( self . react_instrumentStatusChanged )
api . callbacks . instrumentMidiNoteOnActivity . append ( self . react_instrumentMidiNoteOnActivity )
#api.callbacks.instrumentMidiNoteOffActivity.append(self.react_instrumentMidiNoteOffActivity) #We don't react to this here but switch off our indicator with a timer.
#self.treeWidget.setStyleSheet("QTreeWidget::item { border-bottom: 1px solid black;}") #sadly for all items. We want a line below top level items.
if " libraryIsExpanded " in api . session . guiSharedDataToSave :
#This is loaded from JSON, where keys MUST be strings. Our ids got converted into strings on save so we have to convert back to int here.
correctTypesDict = { }
for key , value in api . session . guiSharedDataToSave [ " libraryIsExpanded " ] . items ( ) :
correctTypesDict [ int ( key ) ] = value
api . session . guiSharedDataToSave [ " libraryIsExpanded " ] = correctTypesDict
else :
api . session . guiSharedDataToSave [ " libraryIsExpanded " ] = { } # libId : bool if expanded or not. Also used when switching from nested to flat and back.
#Default values are used in self.buildTree
def reset ( self ) :
""" Used on creation and after resetting the sample dir """
self . _cachedData = None
self . _cachedLastInstrumentStatus = { } # instrument idKey : status Dict
#The next two will delete all children through the garbage collector.
self . guiLibraries = { } # idKey : GuiLibrary idKey is a tuple with second value -1, which would be the instrument.
self . guiInstruments = { } # idKey : GuiInstrument
def itemExpandedOrCollapsed ( self , libraryItem : QtWidgets . QTreeWidgetItem ) :
if type ( libraryItem ) is GuiLibrary : #just in case
api . session . guiSharedDataToSave [ " libraryIsExpanded " ] [ libraryItem . id ] = libraryItem . isExpanded ( )
def currentTreeItemChanged ( self , currentTreeItem : QtWidgets . QTreeWidgetItem ) :
"""
Program wide GUI - only callback from
widget . currentItemChanged - > mainWindow . currentTreeItemChanged . We set the currentItem
ourselves , so we need to block our signals to avoid recursion .
Only one item can be selected at a time .
The currentTreeItem we receive is not a global instance but from a widget different to ours .
We need to find our local version of the same instrument / library / idKey first .
"""
if not self . guiLibraries or not self . guiInstruments :
#"Show Only Loaded" but no instruments are.
#Or Non-Nested view without libraries
return
self . treeWidget . blockSignals ( True )
isLibrary = type ( currentTreeItem ) is GuiLibrary
idKey = currentTreeItem . idKey
if isLibrary and idKey in self . guiLibraries :
assert idKey in self . guiLibraries , ( idKey , self . guiLibraries )
item = self . guiLibraries [ idKey ]
else :
assert idKey in self . guiInstruments , ( idKey , self . guiInstruments )
item = self . guiInstruments [ idKey ]
self . treeWidget . setCurrentItem ( item ) #This will work at first, but as soon as the tab instr/favorites is changed this will jump back to a wrong item (first of the group). We have a tabWidget.changed signal in the main window to reset this
self . treeWidget . blockSignals ( False )
def itemDoubleClicked ( self , item : QtWidgets . QTreeWidgetItem , column : int ) :
""" This chooses the auditioner. Callbacks are handled in the auditioner widget itself """
if type ( item ) is GuiInstrument :
api . auditionerInstrument ( item . idKey )
def isNested ( self ) - > bool :
return self . parentMainWindow . ui . actionFlatNested . isChecked ( )
def isShowOnlyLoadedInstruments ( self ) - > bool :
return self . parentMainWindow . ui . actionShowOnlyLoadedInstruments . isChecked ( )
def buildTree ( self , data : dict ) :
"""
Create the tree . Can be called multiple times and it will re - create itself destructively .
If you call it with data , once at program start , data will get cached . If you call
with data = None it will used the cached variant .
Data is a dict of dicts and has a hierarchy .
data [ libId ] = dictOfInstruments
dictOfInstrument [ instrId ] = pythonDataDict
There is one special " library " key dictOfInstrument [ " library " ] that has metadata for
the lib itself . We use that to construct the top level item and sort all others inside
Example data
{ ' 0 ' : { ' 0 ' : { ' group ' : ' strings ' ,
' id ' : ' 0 ' ,
' idKey ' : ( ' 0 ' , ' 0 ' ) ,
' license ' : ' https://unlicense.org/ ' ,
' name ' : ' Sine Wave ' ,
' tags ' : [ ' sine ' , ' basic ' ] ,
' variants ' : [ ' Sine.sfz ' , ' Sine With 5th Slide.sfz ' ] ,
' vendor ' : ' Test entry to provide more vendor information ' } ,
' 1 ' : { ' group ' : ' strings ' ,
' id ' : ' 1 ' ,
' idKey ' : ( ' 0 ' , ' 1 ' ) ,
' license ' : ' ' ,
' name ' : ' Square Wave ' ,
' tags ' : [ ' square ' , ' basic ' ] ,
' variants ' : [ ' Square.sfz ' , ' Square With 5th Slide.sfz ' ] ,
' vendor ' : ' ' } ,
' 2 ' : { ' group ' : ' brass ' ,
' id ' : ' 2 ' ,
' idKey ' : ( ' 0 ' , ' 2 ' ) ,
' license ' : ' ' ,
' name ' : ' Saw Wave ' ,
' tags ' : [ ' saw ' , ' basic ' ] ,
' variants ' : [ ' Saw.sfz ' , ' Saw With 5th Slide.sfz ' ] ,
' vendor ' : ' ' } ,
' 3 ' : { ' group ' : ' ' ,
' id ' : ' 3 ' ,
' idKey ' : ( ' 0 ' , ' 3 ' ) ,
' license ' : ' ' ,
' name ' : ' Triangle Wave ' ,
' tags ' : [ ' triangle ' , ' complex ' ] ,
' variants ' : [ ' Triangle.sfz ' , ' Triangle With 5th Slide.sfz ' ] ,
' vendor ' : ' ' } ,
' library ' : { ' description ' : ' Basic waveforms. Actual non-looping '
' samples, created by sox. No sfz '
' synth-engine involved. There is a variant '
' with an additional sound-blip for each wave '
' form, which are purely there as technical '
' example. They are not a guideline what '
' constitues a variant and what a different '
' instrument. ' ,
' id ' : ' 0 ' ,
' license ' : ' https://creativecommons.org/publicdomain/zero/1.0/ ' ,
' name ' : ' Tembro Test Instruments ' ,
' tarFilePath ' : PosixPath ( ' /home/nils/lss/test-data.tar ' ) ,
' vendor ' : ' Hilbricht Nils 2021, Laborejo Software Suite '
' https://www.laborejo.org info@laborejo.org ' } } }
"""
#Reset everything except our cached data.
self . treeWidget . clear ( ) #will delete the C++ objects. We need to delete the PyQt objects ourselves, like so:
self . guiLibraries = { } # idKey : GuiLibrary idKey is a tuple with second value -1, which would be the instrument.
self . guiInstruments = { } # idKey : GuiInstrument
if data :
self . _cachedData = data
else :
assert self . _cachedData
data = self . _cachedData
nested = self . isNested ( )
showOnlyLoadedInstruments = self . isShowOnlyLoadedInstruments ( )
for libraryId , libraryDict in data . items ( ) :
if nested :
parentLibraryWidget = GuiLibrary ( parentTreeController = self , libraryDict = libraryDict [ " library " ] )
self . guiLibraries [ ( libraryId , - 1 ) ] = parentLibraryWidget #-1 marks the library but keeps it a tuple.
self . treeWidget . addTopLevelItem ( parentLibraryWidget )
if libraryId in api . session . guiSharedDataToSave [ " libraryIsExpanded " ] :
parentLibraryWidget . setExpanded ( api . session . guiSharedDataToSave [ " libraryIsExpanded " ] [ libraryId ] ) #only possible after gi.init() was done and item inserted.
#parentLibraryWidget.setHidden(True) #only possible after insert
atLeastOneVisible = False
for instrumentdId , instrumentDict in libraryDict . items ( ) :
if instrumentdId == " library " :
#Top level item was already created. Ignore here.
continue
if showOnlyLoadedInstruments and instrumentDict [ " idKey " ] in self . _cachedLastInstrumentStatus and not self . _cachedLastInstrumentStatus [ instrumentDict [ " idKey " ] ] [ " state " ] : #don't create if we only want to see enabled instrument
instrumentVisible = False
elif showOnlyLoadedInstruments and not instrumentDict [ " idKey " ] in self . _cachedLastInstrumentStatus and not instrumentDict [ " status " ] [ " state " ] :
#Not cached yet. This is only good for "load from file". The instruments are not in _cachedLastInstrumentStatus yet
instrumentVisible = False
else :
instrumentVisible = True
atLeastOneVisible = True
gi = GuiInstrument ( parentTreeController = self , instrumentDict = instrumentDict )
if nested :
parentLibraryWidget . addChild ( gi )
else :
self . treeWidget . addTopLevelItem ( gi )
gi . injectWidgets ( ) #only possible after gi.init() was done and item inserted.
self . guiInstruments [ instrumentDict [ " idKey " ] ] = gi
if instrumentDict [ " idKey " ] in self . _cachedLastInstrumentStatus :
gi . updateStatus ( self . _cachedLastInstrumentStatus [ instrumentDict [ " idKey " ] ] )
gi . setHidden ( not instrumentVisible )
#If no instruments were added maybe the lib needs hiding because it is empty
if showOnlyLoadedInstruments and nested and not atLeastOneVisible :
parentLibraryWidget . setHidden ( True )
self . _adjustColumnSize ( )
def showOnlyLoadedInstruments ( self , state : bool ) :
""" The logic is backwards. We receive state=True if instruments should be hidden """
self . buildTree ( data = None ) #uses the menu state for everything except data
def setAllExpanded ( self , state : bool ) :
""" We do not use the qt function collapseAll and expandAll because they do not trigger
the signal """
if self . isNested ( ) :
for idKey , guiLib in self . guiLibraries . items ( ) :
guiLib . setExpanded ( state )
def _adjustColumnSize ( self ) :
self . treeWidget . sortItems ( self . sortByColumnValue , self . sortDescendingValue )
stateIndex = COLUMNS . index ( " state " )
for index in range ( self . treeWidget . columnCount ( ) ) :
if not index == stateIndex :
self . treeWidget . resizeColumnToContents ( index )
self . treeWidget . setColumnWidth ( index , self . treeWidget . columnWidth ( index ) + 15 ) #add padding
#Fixed width for the switch
self . treeWidget . setColumnWidth ( stateIndex , 80 )
def react_instrumentStatusChanged ( self , instrumentStatus : dict ) :
self . parentMainWindow . qtApp . restoreOverrideCursor ( ) #Sometimes the instrument was loaded with a cursor animation
if instrumentStatus [ " idKey " ] in self . guiInstruments :
gi = self . guiInstruments [ instrumentStatus [ " idKey " ] ]
gi . updateStatus ( instrumentStatus )
self . _adjustColumnSize ( )
#We also cache the last status, as we cache the initial data. This way we can delete and recreate TreeItems without requesting new status data from the engine
self . _cachedLastInstrumentStatus [ instrumentStatus [ " idKey " ] ] = instrumentStatus
def react_startLoadingSamples ( self , idKey : tuple ) :
""" Will be overriden by instrument status change / variant chosen """
self . parentMainWindow . qtApp . setOverrideCursor ( QtCore . Qt . WaitCursor ) #reset in self.react_instrumentStatusChanged
text = QtCore . QCoreApplication . translate ( " InstrumentTreeController " , " …loading… " )
loadedIndex = COLUMNS . index ( " loaded " )
instr = self . guiInstruments [ idKey ]
instr . setText ( loadedIndex , text )
self . parentMainWindow . qtApp . processEvents ( ) #actually show the label and cursor
def react_instrumentMidiNoteOnActivity ( self , idKey : tuple , pitch : int , velocity : int ) :
#First figure out which instrument has activity
if idKey in self . guiInstruments :
gi = self . guiInstruments [ idKey ]
gi . activity ( ) #We only do a quick flash here. No need for velocity, pitch or note-off
def contextMenu ( self , qpoint ) : #strange that this is not an event but a qpoint
item = self . treeWidget . itemAt ( qpoint )
if not item :
return
elif type ( item ) is GuiLibrary :
item . contextMenu ( qpoint )
elif type ( item ) is GuiInstrument and not item . state :
return
else : #GuiInstrument and item.state
assert item . state
externalPort = nestedJackPortsMenu ( ) #returns None or clientAndPortString
if externalPort : #None or ClientPortString
if externalPort == - 1 :
externalPort = " "
api . connectInstrumentPort ( item . idKey , externalPort )
class GuiLibrary ( QtWidgets . QTreeWidgetItem ) :
""" The top level library item. All instruments are in a library. """
def __init__ ( self , parentTreeController , libraryDict ) :
super ( ) . __init__ ( [ ] , type = 1000 ) #type 0 is default qt type. 1000 is subclassed user type)
self . id = libraryDict [ " id " ]
self . idKey = ( libraryDict [ " id " ] , - 1 ) #fake it for compatibility. -1 means library
self . name = libraryDict [ " name " ]
#No dynamic data here. Everything gets created once.
#self.setText(COLUMNS.index("idKey"), str(libraryDict["id"]).zfill(leadingZeroesForZfill))
self . setData ( COLUMNS . index ( " idKey " ) , 0 , int ( libraryDict [ " id " ] ) ) #set data allows sorting by actual numbers. 0 is the data role, which is just "display text".
self . setText ( COLUMNS . index ( " name " ) , str ( libraryDict [ " name " ] ) )
self . setText ( COLUMNS . index ( " tags " ) , str ( libraryDict [ " description " ] ) [ : 42 ] + " … " )
#Hack the row height through an unused column.
#self.setText(COLUMNS.index("loaded"), "")
#We cannot call setExpanded here. The item must first be inserted into the parent tree.
#We placed this call in InstrumentTreeController.buildTree
#self.setExpanded(False)
icon = parentTreeController . parentMainWindow . style ( ) . standardIcon ( getattr ( QtWidgets . QStyle , " SP_DirIcon " ) )
self . setIcon ( COLUMNS . index ( " name " ) , icon )
def contextMenu ( self , qpoint ) :
""" This isn ' t the qt function, but we call this from self.parentTreeController.
Logically it makes sense to have it here though """
menu = QtWidgets . QMenu ( )
listOfLabelsAndFunctions = [
( QtCore . QCoreApplication . translate ( " GuiLibraryContextMenu " , " Load whole Library " ) , lambda : api . loadLibraryInstrumentSamples ( self . id ) ) ,
( QtCore . QCoreApplication . translate ( " GuiLibraryContextMenu " , " Unload whole Library " ) , lambda : api . unloadLibraryInstrumentSamples ( self . id ) ) ,
]
for text , function in listOfLabelsAndFunctions :
if function is None :
l = QtWidgets . QLabel ( text )
l . setAlignment ( QtCore . Qt . AlignCenter )
a = QtWidgets . QWidgetAction ( menu )
a . setDefaultWidget ( l )
menu . addAction ( a )
else :
a = QtWidgets . QAction ( text , menu )
menu . addAction ( a )
a . triggered . connect ( function )
pos = QtGui . QCursor . pos ( )
pos . setY ( pos . y ( ) + 5 )
menu . exec_ ( pos )
class GuiInstrument ( QtWidgets . QTreeWidgetItem ) :
"""
Why is this not a QTableWidget ? As in Agordejo , a TableWidget is a complex item , and inconvenient
to use . You need to add an Item to each cell . While in TreeWidget you just create one item .
All data is received at program start . No new items will be created , none will get deleted .
All instruments in Tembro are static .
Most parameters we receive are read only , like instrId , name and version
Not all parameters are used in the TreeWidgetItem . e . g . Description , Vendor and License are only
for the details view .
"""
def __init__ ( self , parentTreeController , instrumentDict ) :
self . parentTreeController = parentTreeController
self . idKey = instrumentDict [ " idKey " ]
#Start with empty columns. We fill in later in _writeColumns
super ( ) . __init__ ( [ ] , type = 1000 ) #type 0 is default qt type. 1000 is subclassed user type)
self . columns = COLUMNS
#Use with:
#nameColumnIndex = self.columns.index("prettyName")
#self.setText(nameColumnIndex, "hello")
self . setTextAlignment ( self . columns . index ( " idKey " ) , QtCore . Qt . AlignHCenter )
self . state = None #by self.update...
self . instrumentDict = None
self . _writeColumns ( instrumentDict )
self . mutedMixSendStylesheet = """
QSlider : : handle : horizontal {
background : #5c0000;
border - radius : 4 px ;
height : 8 px ;
}
"""
self . mixSendDial = QtWidgets . QSlider ( QtCore . Qt . Horizontal )
self . mixSendDial . setMaximumSize ( QtCore . QSize ( 48 , 16 ) )
self . mixSendDial . setPageStep ( 3 )
self . mixSendDial . setMaximum ( 0 )
self . mixSendDial . setMinimum ( - 21 )
self . mixSendDial . setValue ( - 3 )
self . mixSendDial . valueChanged . connect ( self . _sendVolumeChangeToEngine )
self . mixSendDial . enterEvent = lambda ev : self . parentTreeController . parentMainWindow . statusBar ( ) . showMessage ( ( QtCore . QCoreApplication . translate ( " Instrument " , " Use mousewheel to change the instruments mixSend for the stereo mixer ouput. Right click to (un)mute mixer-send. " ) ) )
self . mixSendDial . leaveEvent = lambda ev : self . parentTreeController . parentMainWindow . statusBar ( ) . showMessage ( " " )
self . leaveEvent = lambda ev : self . parentTreeController . parentMainWindow . statusBar ( ) . showMessage ( " " )
self . mixSendDial . contextMenuEvent = self . _mixSendDialContextMenuEvent
self . mixSendDial . setEnabled ( False ) #default is off, so we don't send mixSend changes for an unloaded instrument
self . toggleSwitch = FancySwitch ( track_radius = 8 , thumb_radius = 7 ) #radius is the actual size, not the rounded corners.
#self.toggleSwitch.toggled.connect(lambda c: print('toggled', c)) #triggered by engine callback as well
self . toggleSwitch . setAutoFillBackground ( True ) #otherwise conflicts with setItemWidget
self . toggleSwitch . clicked . connect ( self . instrumentSwitchOnViaGui )
self . toggleSwitch . leaveEvent = lambda ev : self . parentTreeController . parentMainWindow . statusBar ( ) . showMessage ( " " )
#self.toggleSwitch.pressed.connect(lambda: print('pressed')) #literal mouse down.
#self.toggleSwitch.released.connect(lambda: print('released'))
#We cannot add the ToggleSwitch Widget here.
#It must be inserted after self was added to the Tree. Use self.injectToggleSwitch from parent
#icon = parentTreeController.parentMainWindow.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ComputerIcon"))
#px = QtGui.QPixmap(32,32) #TODO: get size from standard icon above. but how? Apparently it does not matter how big this is.
#px.fill(QtGui.QColor(255,255,255,0)) #transparent icon)
#icon = QtGui.QIcon(px)
#self.setIcon(COLUMNS.index("name"), icon)
#Create icons for midi in status
on = QtGui . QPixmap ( 32 , 32 )
oncolor = QtGui . QColor ( " cyan " )
on . fill ( oncolor )
self . onIcon = QtGui . QIcon ( on )
off = QtGui . QPixmap ( 32 , 32 )
#offcolor = QtGui.QColor(50, 50, 50) #dark grey
offcolor = QtGui . QColor ( 255 , 255 , 255 , 0 ) #transparent
off . fill ( offcolor )
self . offIcon = QtGui . QIcon ( off )
self . setIcon ( COLUMNS . index ( " name " ) , self . offIcon )
self . midiActiveFlag = False #midi indicator
api . session . eventLoop . verySlowConnect ( self . _activityOff ) #this is always on, even if no samples loaded. The auditioner sends activeOn, even if state==False
def instrumentSwitchOnViaGui ( self , state ) :
""" Only GUI clicks. Does not react to the engine callback that switches on instruments. For
example one that arrives through " load group " or " load all "
We use this to count , across program runs , how many times this instrument was activated
and present it as favourites . The engine does not know about this .
Loading states from save files , " load all " and other algorithmic loaders do not contribute
to this counting .
"""
if state :
api . loadInstrumentSamples ( self . idKey )
self . parentTreeController . parentMainWindow . favoriteInstrument ( self . idKey )
else :
api . unloadInstrumentSamples ( self . idKey )
def updateStatus ( self , instrumentStatus : dict ) :
#Before we set the state permanently we use the opportunity to see if this is program state (state == None) or unloading
self . cachedInstrumentStatus = instrumentStatus
firstLoad = self . state is None
variantColumnIndex = self . columns . index ( " loaded " )
self . currentVariant = instrumentStatus [ " currentVariant " ]
#if instrumentStatus["currentVariant"]: #None if not loaded or not enabled anymore
self . setText ( variantColumnIndex , instrumentStatus [ " currentVariant " ] . rstrip ( " .sfz " ) ) #either "" or a variant
self . state = instrumentStatus [ " state " ] #is no bool and not None.
self . toggleSwitch . setChecked ( instrumentStatus [ " state " ] )
self . mixSendDial . setStyleSheet ( " " )
self . mixSendDial . setUpdatesEnabled ( True )
if self . state :
#either reload or load for the first time
self . mixSendDial . setEnabled ( True )
#Qslider can only have integer values. mixerLevel" is float.
#We simply drop the float part, it doesn't matter for our coarse in-house mixing.
self . mixSendDial . setValue ( int ( instrumentStatus [ " mixerLevel " ] ) )
if instrumentStatus [ " mixerEnabled " ] :
muteText = " "
else :
muteText = QtCore . QCoreApplication . translate ( " Instrument " , " [Muted] " )
self . mixSendDial . setStyleSheet ( self . mutedMixSendStylesheet )
self . parentTreeController . parentMainWindow . statusBar ( ) . showMessage ( ( QtCore . QCoreApplication . translate ( " Instrument " , " {} send to mix volume: {} {} " . format ( self . instrumentDict [ " name " ] , instrumentStatus [ " mixerLevel " ] , muteText ) ) ) )
#api.session.eventLoop.verySlowConnect(self._activityOff) #this is always on, even if not loaded. The auditioner sends activeOn, even if state==False
elif not firstLoad : #and not self.state
#the instrument was once loaded and is currently connected
pass
#api.session.eventLoop.verySlowDisconnect(self._activityOff)
if not self . state : #in any not-case
self . mixSendDial . setEnabled ( False )
self . mixSendDial . setUpdatesEnabled ( False ) #this is a hack to make the widget disappear. Because hiding a QTreeWidgetItem does not work.
self . parentTreeController . parentMainWindow . qtApp . processEvents ( ) #actually show the new state, even if mass unloading (which did not work before. But loading worked!)
def _mixSendDialContextMenuEvent ( self , event ) :
if self . cachedInstrumentStatus [ " mixerEnabled " ] :
mixerMuteText = QtCore . QCoreApplication . translate ( " InstrumentMixerLevelContextMenu " , " Mute/Disable Mixer-Send for {} " . format ( self . instrumentDict [ " name " ] ) )
mixerMuteFunc = lambda : api . setInstrumentMixerEnabled ( self . idKey , False )
else :
mixerMuteText = QtCore . QCoreApplication . translate ( " InstrumentMixerLevelContextMenu " , " Unmute/Enable Mixer-Send for {} " . format ( self . instrumentDict [ " name " ] ) )
mixerMuteFunc = lambda : api . setInstrumentMixerEnabled ( self . idKey , True )
listOfLabelsAndFunctions = [
( mixerMuteText , mixerMuteFunc ) ,
]
menu = QtWidgets . QMenu ( )
for text , function in listOfLabelsAndFunctions :
if function is None :
l = QtWidgets . QLabel ( text )
l . setAlignment ( QtCore . Qt . AlignCenter )
a = QtWidgets . QWidgetAction ( menu )
a . setDefaultWidget ( l )
menu . addAction ( a )
else :
a = QtWidgets . QAction ( text , menu )
menu . addAction ( a )
a . triggered . connect ( function )
pos = QtGui . QCursor . pos ( )
pos . setY ( pos . y ( ) + 5 )
menu . exec_ ( pos )
def injectWidgets ( self ) :
""" Call this after the item was added to the tree. Widgets must be inserted after the Item
was created """
stateColumnIndex = self . columns . index ( " state " )
self . parentTreeController . treeWidget . setItemWidget ( self , stateColumnIndex , self . toggleSwitch )
mixSendColumnIndex = self . columns . index ( " mixSend " )
self . parentTreeController . treeWidget . setItemWidget ( self , mixSendColumnIndex , self . mixSendDial )
def _writeColumns ( self , instrumentDict ) :
""" This is used to construct the columns when the program starts. There is an
update callback for dynamic values as well """
self . instrumentDict = instrumentDict
for index , key in enumerate ( self . columns ) :
QtCore . QCoreApplication . translate ( " OpenSession " , " not saved " )
if key == " state " or key == " loaded " or key == " mixSend " :
pass #this arrives through a different api.callback
elif type ( instrumentDict [ key ] ) is str :
self . setText ( index , str ( instrumentDict [ key ] ) )
elif key == " tags " : #list
t = " , " . join ( instrumentDict [ key ] )
self . setText ( index , t )
elif key == " idKey " : #tuple
libId , instrId = instrumentDict [ key ]
zeros = int ( instrumentDict [ " instrumentsInLibraryCount " ] / 10 ) + 1
instIdZFilled = str ( instrId ) . zfill ( zeros )
if self . parentTreeController . isNested ( ) :
self . setText ( index , instIdZFilled )
else : #full id
#self.setText(index, f"{libId}-{str(instrId).zfill(zeros)}")
self . setData ( index , 0 , float ( str ( libId ) + " . " + instIdZFilled ) ) #0 is the data role, just standard display text. We combine both IDs to a float number for sorting. If used with setData instead of setText Qt will know how to sort 11 before 1000
"""
elif key == " state " : #use parameter for initial value. loaded from file or default = False.
state = instrumentDict [ key ]
assert type ( state ) is bool , state
self . switch ( state )
"""
def _activityOff ( self ) :
""" Called by a timer """
if self . midiActiveFlag :
self . midiActiveFlag = False
self . setIcon ( COLUMNS . index ( " name " ) , self . offIcon )
def activity ( self ) :
""" Show midi note ons as flashing light. """
self . midiActiveFlag = True
self . setIcon ( COLUMNS . index ( " name " ) , self . onIcon )
def _sendVolumeChangeToEngine ( self , newValue ) :
self . mixSendDial . blockSignals ( True )
api . setInstrumentMixerVolume ( self . idKey , newValue )
self . mixSendDial . blockSignals ( False )