#! /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 . """ import logging; logging.info("import {}".format(__file__)) #Standard Library Modules #Third Party Modules from PyQt5 import QtWidgets, QtCore, QtGui from 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.""" def __init__(self, parentWidget): 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) 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) self.cboxPortname, self.cboxMidiPortUid = api.getMidiInputNameAndUuid() #these don't change during runtime. 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.""" try: currentConnectedList = cbox.JackIO.get_connected_ports(self.cboxMidiPortUid) except: #port not found. currentConnectedList = [] for port in currentConnectedList: cbox.JackIO.port_disconnect(port, self.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, self.cboxPortname)