You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
221 lines
9.0 KiB
221 lines
9.0 KiB
#! /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
|
|
|
|
#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.
|
|
"""
|
|
|
|
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))
|
|
|
|
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).
|
|
"""
|
|
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])
|
|
|
|
|
|
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
|
|
|