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.
330 lines
14 KiB
330 lines
14 KiB
#! /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
|
|
|
|
#Engine
|
|
import engine.api as api
|
|
|
|
|
|
COLUMNS = ("state", "id-key", "name", "loaded", "group", "tags" ) #Loaded = Variant
|
|
|
|
|
|
class InstrumentTreeController(object):
|
|
"""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.guiInstruments = {} # id-key : GuiInstrument
|
|
|
|
|
|
self.headerLabels = [
|
|
" ",
|
|
QtCore.QCoreApplication.translate("InstrumentTreeController", "ID"),
|
|
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.sortByColumnValue = 1 #by instrId
|
|
self.sortDescendingValue = 0 # Qt::SortOrder which is 0 for ascending and 1 for descending
|
|
|
|
api.callbacks.instrumentListMetadata.append(self.buildTree)
|
|
api.callbacks.startLoadingSamples.append(self.react_startLoadingSamples)
|
|
api.callbacks.instrumentStatusChanged.append(self.react_instrumentStatusChanged)
|
|
|
|
|
|
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()
|
|
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):
|
|
if type(item) is GuiInstrument:
|
|
api.auditionerInstrument(item.idkey)
|
|
|
|
|
|
def buildTree(self, data:dict):
|
|
"""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'}}}
|
|
"""
|
|
|
|
for libraryId, libraryDict in data.items():
|
|
|
|
parentLibraryWidget = GuiLibrary(parentTreeController=self, libraryDict=libraryDict["library"])
|
|
self.treeWidget.addTopLevelItem(parentLibraryWidget)
|
|
|
|
for instrumentdId, instrumentDict in libraryDict.items():
|
|
if instrumentdId == "library":
|
|
#Top level item was already created. Ignore here.
|
|
pass
|
|
else:
|
|
self.newInstrument(parentLibraryWidget, instrumentDict)
|
|
|
|
parentLibraryWidget.setExpanded(True)
|
|
|
|
self._adjustColumnSize()
|
|
|
|
|
|
def newInstrument(self, parentLibraryWidget, instrumentDict):
|
|
gi = GuiInstrument(parentTreeController=self, instrumentDict=instrumentDict)
|
|
#self.treeWidget.addTopLevelItem(gi)
|
|
parentLibraryWidget.addChild(gi)
|
|
gi.injectToggleSwitch() #only possible after gi.init was done and item inserted.
|
|
self._adjustColumnSize()
|
|
self.guiInstruments[instrumentDict["id-key"]] = gi
|
|
|
|
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):
|
|
gi = self.guiInstruments[instrumentStatus["id-key"]]
|
|
gi.updateStatus(instrumentStatus)
|
|
|
|
|
|
def react_startLoadingSamples(self, idkey:tuple):
|
|
"""Will be overriden by instrument status change / variant chosen"""
|
|
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
|
|
|
|
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.idkey = (libraryDict["id"], 0) #fake it for compatibility
|
|
|
|
#No dynamic data here. Everything gets created once.
|
|
self.setText(COLUMNS.index("id-key"), str(libraryDict["id"]))
|
|
self.setText(COLUMNS.index("name"), str(libraryDict["name"]))
|
|
self.setText(COLUMNS.index("tags"), str(libraryDict["description"])[:42]+"…")
|
|
|
|
|
|
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.switch()
|
|
self.instrumentDict = None
|
|
self._writeColumns(instrumentDict)
|
|
|
|
self.toggleSwitch = ToggleSwitch()
|
|
self.toggleSwitch.setAutoFillBackground(True) #otherwise conflicts with setItemWidget
|
|
#self.toggleSwitch.toggled.connect(lambda c: print('toggled', c)) #triggered by engine callback as well
|
|
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
|
|
|
|
|
|
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):
|
|
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"))
|
|
self.state = instrumentStatus["state"]
|
|
self.toggleSwitch.setChecked(instrumentStatus["state"])
|
|
|
|
def injectToggleSwitch(self):
|
|
"""Call this after the item was added to the tree"""
|
|
stateColumnIndex = self.columns.index("state")
|
|
self.parentTreeController.treeWidget.setItemWidget(self, stateColumnIndex, self.toggleSwitch)
|
|
|
|
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":
|
|
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]
|
|
self.setText(index, f"{libId}-{instrId}")
|
|
|
|
"""
|
|
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 switch(self, state:bool):
|
|
"""This is not the Qt function but if an instrument is enabled, loaded to RAM and ready to
|
|
receive midi data.
|
|
|
|
Function will mimic Qt disabled behaviour by greying things out and deactivating individual
|
|
sub-widgets. But some, like the GUI switch itself, will always stay enabled."""
|
|
self.state = state
|
|
|
|
def toggleSwitchState(self):
|
|
self.switch(not self.state)
|
|
|