#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021 , 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
#Engine
import engine . api as api
COLUMNS = ( " state " , " id-key " , " 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 .
And we might use the TreeView to group by manufacturer etc . Eventhough we have a Filter .
"""
def __init__ ( self , parentMainWindow ) :
self . parentMainWindow = parentMainWindow
self . treeWidget = self . parentMainWindow . ui . instruments_treeWidget
self . _cachedData = None
self . _cachedLastInstrumentStatus = { } # instrument idkey : status Dict
self . guiLibraries = { } # id-key : GuiLibrary
self . guiInstruments = { } # id-key : GuiInstrument
self . currentlyNested = None #is the view nested in libraries or just all instruments?
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 . setAlternatingRowColors ( True )
self . treeWidget . setContextMenuPolicy ( QtCore . Qt . CustomContextMenu )
self . treeWidget . header ( ) . setSortIndicator ( 0 , 0 )
self . treeWidget . itemDoubleClicked . connect ( self . itemDoubleClicked )
self . treeWidget . itemSelectionChanged . connect ( self . itemSelectionChanged )
self . treeWidget . itemExpanded . connect ( self . itemExpandedOrCollapsed )
self . treeWidget . itemCollapsed . connect ( self . itemExpandedOrCollapsed )
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 )
#self.treeWidget.setStyleSheet("QTreeWidget::item { border-bottom: 1px solid black;}") #sadly for all items. We want a line below top level items.
if not " libraryIsExpanded " in api . session . guiSharedDataToSave :
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 itemExpandedOrCollapsed ( self , libraryItem : QtWidgets . QTreeWidgetItem ) :
#print (libraryItem.name, libraryItem.isExpanded())
api . session . guiSharedDataToSave [ " libraryIsExpanded " ] [ libraryItem . id ] = libraryItem . isExpanded ( )
def itemSelectionChanged ( self ) :
""" Only one instrument can be selected at the same time.
This function mostly informs other widgets that a different instrument was selected
"""
selItems = self . treeWidget . selectedItems ( )
if not selItems :
return
assert len ( selItems ) == 1 , selItems #because our selection is set to single.
item = selItems [ 0 ]
if type ( item ) is GuiInstrument :
self . parentMainWindow . selectedInstrumentController . instrumentChanged ( item . idkey )
else :
self . parentMainWindow . selectedInstrumentController . directLibrary ( item . idkey )
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 buildTree ( self , data : dict , nested : bool = None ) :
"""
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 ' ,
' id-key ' : ( ' 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 ' ,
' id-key ' : ( ' 0 ' , ' 1 ' ) ,
' license ' : ' ' ,
' name ' : ' Square Wave ' ,
' tags ' : [ ' square ' , ' basic ' ] ,
' variants ' : [ ' Square.sfz ' , ' Square With 5th Slide.sfz ' ] ,
' vendor ' : ' ' } ,
' 2 ' : { ' group ' : ' brass ' ,
' id ' : ' 2 ' ,
' id-key ' : ( ' 0 ' , ' 2 ' ) ,
' license ' : ' ' ,
' name ' : ' Saw Wave ' ,
' tags ' : [ ' saw ' , ' basic ' ] ,
' variants ' : [ ' Saw.sfz ' , ' Saw With 5th Slide.sfz ' ] ,
' vendor ' : ' ' } ,
' 3 ' : { ' group ' : ' ' ,
' id ' : ' 3 ' ,
' id-key ' : ( ' 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 = { } # id-key : GuiLibrary
self . guiInstruments = { } # id-key : GuiInstrument
self . currentlyNested = nested
if data :
self . _cachedData = data
else :
assert self . _cachedData
data = self . _cachedData
if nested is None :
if " nestedView " in api . session . guiSharedDataToSave :
nested = api . session . guiSharedDataToSave [ " nestedView " ]
else :
nested = True
api . session . guiSharedDataToSave [ " nestedView " ] = nested
self . currentlyNested = nested
for libraryId , libraryDict in data . items ( ) :
if nested :
parentLibraryWidget = GuiLibrary ( parentTreeController = self , libraryDict = libraryDict [ " library " ] )
self . guiLibraries [ libraryId ] = parentLibraryWidget
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
for instrumentdId , instrumentDict in libraryDict . items ( ) :
if instrumentdId == " library " :
#Top level item was already created. Ignore here.
pass
else :
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 [ " id-key " ] ] = gi
if instrumentDict [ " id-key " ] in self . _cachedLastInstrumentStatus :
gi . updateStatus ( self . _cachedLastInstrumentStatus [ instrumentDict [ " id-key " ] ] )
self . _adjustColumnSize ( )
def toggleNestedFlat ( self , newCheckStateIsNested ) :
""" We receive newCheckState as automatic parameter from Qt from the calling menu action """
self . buildTree ( data = None , nested = newCheckStateIsNested ) #with data=None it will used the cache data we received once, at startup
def setAllExpanded ( self , state : bool ) :
""" We do not use the qt function collapseAll and expandAll because they do not trigger
the signal """
if self . currentlyNested :
for libid , guiLib in self . guiLibraries . items ( ) :
guiLib . setExpanded ( state ) #triggers signal which will trigger self.toggleNestedFlat
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
gi = self . guiInstruments [ instrumentStatus [ " id-key " ] ]
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 [ " id-key " ] ] = 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 ) :
#First figure out which instrument has activity
gi = self . guiInstruments [ idkey ]
gi . activity ( )
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 " ] , 0 ) #fake it for compatibility
self . name = libraryDict [ " name " ]
#No dynamic data here. Everything gets created once.
#self.setText(COLUMNS.index("id-key"), str(libraryDict["id"]).zfill(leadingZeroesForZfill))
self . setData ( COLUMNS . index ( " id-key " ) , 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 )
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 .
"""
allItems = { } # instrId : GuiInstrument
def __init__ ( self , parentTreeController , instrumentDict ) :
GuiInstrument . allItems [ instrumentDict [ " id-key " ] ] = self
self . parentTreeController = parentTreeController
self . idkey = instrumentDict [ " id-key " ]
#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 ( " id-key " ) , 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 . 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.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
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 " """
if state :
api . loadInstrumentSamples ( 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 " ]
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 )
self . mixSendDial . setValue ( 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 )
elif not firstLoad : #and not self.state
#the instrument was once loaded and is currently connected
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.
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 == " id-key " : #tuple
libId , instrId = instrumentDict [ key ]
zeros = int ( instrumentDict [ " instrumentsInLibraryCount " ] / 10 ) + 1
instIdZFilled = str ( instrId ) . zfill ( zeros )
if self . parentTreeController . currentlyNested :
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 )