#! /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 ; logging . info ( " import {} " . format ( __file__ ) )
#Standard Library Modules
#Third Party Modules
from PyQt5 import QtWidgets , QtCore , QtGui
from template . calfbox import cbox
#Template Modules
#Our modules
import engine . api as api
class QuickMidiInputComboController ( QtWidgets . QWidget ) :
""" This widget breaks a bit with the convention of engine/gui. However, it is a pure convenience
fire - and - forget function with no callbacks .
If you give a text it must already be translated . """
def __init__ ( self , parentWidget , text = None , stretch = True ) :
super ( ) . __init__ ( parentWidget )
self . parentWidget = parentWidget
self . layout = QtWidgets . QHBoxLayout ( )
self . setLayout ( self . layout )
self . comboBox = QtWidgets . QComboBox ( self )
self . layout . addWidget ( self . comboBox )
"""
self . volumeDial = parentWidget . ui . auditionerVolumeDial
self . volumeDial . setMaximum ( 0 )
self . volumeDial . setMinimum ( - 21 )
self . volumeDial . valueChanged . connect ( self . _sendVolumeChangeToEngine )
self . volumeDial . enterEvent = lambda ev : self . parentWidget . statusBar ( ) . showMessage ( ( QtCore . QCoreApplication . translate ( " Auditioner " , " Use mousewheel to change the Auditioner Volume " ) ) )
self . volumeDial . leaveEvent = lambda ev : self . parentWidget . statusBar ( ) . showMessage ( " " )
self . wholePanel = parentWidget . ui . auditionerWidget
"""
self . label = QtWidgets . QLabel ( self )
if text :
self . label . setText ( text ) #already translated by parent.
else :
self . label . setText ( QtCore . QCoreApplication . translate ( " QuickMidiInputComboController " , " Midi Input. Use JACK to connect multiple inputs. " ) )
self . layout . addWidget ( self . label )
#if not api.isStandaloneMode():
#self.wholePanel.hide()
#return
##self.wholePanel.show() #explicit is better than implicit
self . originalShowPopup = self . comboBox . showPopup
self . comboBox . showPopup = self . showPopup
self . comboBox . activated . connect ( self . _newPortChosen )
if stretch :
self . layout . addStretch ( )
#api.callbacks.startLoadingAuditionerInstrument.append(self.callback_startLoadingAuditionerInstrument)
#api.callbacks.auditionerInstrumentChanged.append(self.callback_auditionerInstrumentChanged)
#api.callbacks.auditionerVolumeChanged.append(self.callback__auditionerVolumeChanged)
def callback_startLoadingAuditionerInstrument ( self , idkey ) :
self . parentWidget . qtApp . setOverrideCursor ( QtCore . Qt . WaitCursor ) # type: ignore #mypy doesn't know PyQts parent Widget #reset in self.callback_auditionerInstrumentChanged
self . label . setText ( QtCore . QCoreApplication . translate ( " QuickMidiInputComboController " , " …loading… " ) )
self . parentWidget . qtApp . processEvents ( ) #actually show the label and cursor
def callback_auditionerInstrumentChanged ( self , exportMetadata : dict ) :
app = self . parentWidget . qtApp . restoreOverrideCursor ( ) # type: ignore #mypy doesn't know PyQts parent Widget #We assume the cursor was set to a loading animation
key = exportMetadata [ " id-key " ]
t = f " ➜ [ { key [ 0 ] } - { key [ 1 ] } ] { exportMetadata [ ' name ' ] } "
self . label . setText ( t )
def callback__auditionerVolumeChanged ( self , value : float ) :
self . volumeDial . setValue ( value )
self . parentWidget . statusBar ( ) . showMessage ( QtCore . QCoreApplication . translate ( " QuickMidiInputComboController " , " Volume: {} " ) . format ( value ) ) # type: ignore #mypy doesn't know PyQts parent Widget
def _sendVolumeChangeToEngine ( self , newValue ) :
self . volumeDial . blockSignals ( True )
api . setAuditionerVolume ( newValue )
self . volumeDial . blockSignals ( False )
def _newPortChosen ( self , index : int ) :
assert self . comboBox . currentIndex ( ) == index
self . connectMidiInputPort ( self . comboBox . currentText ( ) )
def showPopup ( self ) :
""" When the combobox is opened quickly update the port list before showing it """
self . _fill ( )
self . originalShowPopup ( )
def _fill ( self ) :
self . comboBox . clear ( )
self . comboBox . setSizeAdjustPolicy ( QtWidgets . QComboBox . AdjustToContents )
availablePorts = self . getAvailablePorts ( )
self . comboBox . addItem ( " " ) # Not only a more visible seaparator than the Qt one, but also doubles as "disconnect"
self . comboBox . addItems ( availablePorts [ " hardware " ] )
#self.comboBox.insertSeparator(len(availablePorts["hardware"])+1)
self . comboBox . addItem ( " " ) # Not only a more visible seaparator than the Qt one, but also doubles as "disconnect"
self . comboBox . addItems ( availablePorts [ " software " ] )
def getAvailablePorts ( self ) - > dict :
""" This function queries JACK each time it is called.
It returns a dict with two lists .
Keys " hardware " and " software " for the type of port .
"""
result = { }
hardware = set ( cbox . JackIO . get_ports ( " .* " , cbox . JackIO . MIDI_TYPE , cbox . JackIO . PORT_IS_SOURCE | cbox . JackIO . PORT_IS_PHYSICAL ) )
allPorts = set ( cbox . JackIO . get_ports ( " .* " , cbox . JackIO . MIDI_TYPE , cbox . JackIO . PORT_IS_SOURCE ) )
software = allPorts . difference ( hardware )
result [ " hardware " ] = sorted ( list ( hardware ) )
result [ " software " ] = sorted ( list ( software ) )
return result
def connectMidiInputPort ( self , externalPort : str ) :
""" externalPort is in the Client:Port JACK format
If " " False or None disconnect all ports . """
cboxPortname , cboxMidiPortUid = api . getMidiInputNameAndUuid ( ) #these don't change during runtime. But if the system it not ready yet it returns None, None
if cboxPortname is None and cboxMidiPortUid is None :
logging . info ( " engine is not ready yet to deliver midi input name and port id. This is normal during startup but a problem during normal runtime. " )
return #startup delay
try :
currentConnectedList = cbox . JackIO . get_connected_ports ( cboxMidiPortUid )
except : #port not found.
currentConnectedList = [ ]
for port in currentConnectedList :
cbox . JackIO . port_disconnect ( port , cboxPortname )
if externalPort :
availablePorts = self . getAvailablePorts ( )
if not ( externalPort in availablePorts [ " hardware " ] or externalPort in availablePorts [ " software " ] ) :
raise RuntimeError ( f " QuickMidiInput was instructed to connect to port { externalPort } , which does not exist " )
cbox . JackIO . port_connect ( externalPort , cboxPortname )