#! /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 . """ 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. The engine has no concept of "selected instrument". This is purely on our GUI side. We relay this information not only to our own information widgets but also to other widgets, like the piano. """ 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)) #Inform other widgets (but not recursively ourselves) that we clicked on no instrument. #e.g. to switch off the gui keyboards. self.parentMainWindow.selectedInstrumentChanged(None, None) 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\nVendor: " + metadata["vendor"] + "\n\nLicense: " + metadata["license"] else: fullText = metadata["description"] + "\n\nVendor: " + 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). We also relay this information to the main window which can send it to other widgets, like the two keyboards. We will not receive this callback from the mainwindow. """ 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]) #Relay to Main Window. We will not receive this ourselves. self.parentMainWindow.selectedInstrumentChanged(instrumentStatus, instrumentData) 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