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.
640 lines
31 KiB
640 lines
31 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
|
|
from template.qtgui.helper import ToggleSwitch,FancySwitch
|
|
from template.qtgui.submenus import nestedJackPortsMenu
|
|
|
|
#Engine
|
|
import engine.api as api
|
|
|
|
|
|
COLUMNS = ("state", "idKey", "mixSend", "name", "loaded", "group", "tags" ) #Loaded = Variant
|
|
|
|
|
|
class InstrumentTreeController(object):
|
|
"""
|
|
Shows the list of instruments, so they can be clicked upon :)
|
|
|
|
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.
|
|
"""
|
|
|
|
def __init__(self, parentMainWindow, treeWidget):
|
|
self.parentMainWindow = parentMainWindow
|
|
self.treeWidget = treeWidget
|
|
|
|
self.reset()
|
|
#Includes:
|
|
#self._cachedData = None
|
|
#self._cachedLastInstrumentStatus = {} # instrument idKey : status Dict
|
|
#self.guiLibraries = {} # idKey : GuiLibrary. idKey is a tuple with second value -1, which would be the instrument.
|
|
#self.guiInstruments = {} # idKey : GuiInstrument
|
|
|
|
|
|
self.headerLabels = [
|
|
QtCore.QCoreApplication.translate("InstrumentTreeController", "Enable"),
|
|
QtCore.QCoreApplication.translate("InstrumentTreeController", "ID"),
|
|
QtCore.QCoreApplication.translate("InstrumentTreeController", "toMix"),
|
|
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.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
|
self.treeWidget.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
|
self.treeWidget.setAlternatingRowColors(True)
|
|
self.treeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
|
|
self.treeWidget.header().setSortIndicator(0,0)
|
|
|
|
self.treeWidget.itemDoubleClicked.connect(self.itemDoubleClicked)
|
|
self.treeWidget.currentItemChanged.connect(self.parentMainWindow.currentTreeItemChanged)
|
|
#self.treeWidget.itemSelectionChanged.connect(self.itemSelectionChanged) #also triggers on tab change between favorits and instruments
|
|
#self.treeWidget.itemClicked.connect(self.itemSelectionChanged) #This will not activate when using the arrow keys to select
|
|
self.treeWidget.itemExpanded.connect(self.itemExpandedOrCollapsed)
|
|
self.treeWidget.itemCollapsed.connect(self.itemExpandedOrCollapsed)
|
|
self.treeWidget.customContextMenuRequested.connect(self.contextMenu)
|
|
|
|
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) #called without arguments it will just create the standard tree. This will only happen once at program start.
|
|
api.callbacks.startLoadingSamples.append(self.react_startLoadingSamples)
|
|
api.callbacks.instrumentStatusChanged.append(self.react_instrumentStatusChanged)
|
|
api.callbacks.instrumentMidiNoteOnActivity.append(self.react_instrumentMidiNoteOnActivity)
|
|
#api.callbacks.instrumentMidiNoteOffActivity.append(self.react_instrumentMidiNoteOffActivity) #We don't react to this here but switch off our indicator with a timer.
|
|
|
|
#self.treeWidget.setStyleSheet("QTreeWidget::item { border-bottom: 1px solid black;}") #sadly for all items. We want a line below top level items.
|
|
|
|
if "libraryIsExpanded" in api.session.guiSharedDataToSave:
|
|
#This is loaded from JSON, where keys MUST be strings. Our ids got converted into strings on save so we have to convert back to int here.
|
|
correctTypesDict = {}
|
|
for key, value in api.session.guiSharedDataToSave["libraryIsExpanded"].items():
|
|
correctTypesDict[int(key)] = value
|
|
api.session.guiSharedDataToSave["libraryIsExpanded"] = correctTypesDict
|
|
else:
|
|
api.session.guiSharedDataToSave["libraryIsExpanded"] = {} # libId : bool if expanded or not. Also used when switching from nested to flat and back.
|
|
#Default values are used in self.buildTree
|
|
|
|
|
|
def reset(self):
|
|
"""Used on creation and after resetting the sample dir"""
|
|
self._cachedData = None
|
|
self._cachedLastInstrumentStatus = {} # instrument idKey : status Dict
|
|
#The next two will delete all children through the garbage collector.
|
|
self.guiLibraries = {} # idKey : GuiLibrary idKey is a tuple with second value -1, which would be the instrument.
|
|
self.guiInstruments = {} # idKey : GuiInstrument
|
|
|
|
|
|
def itemExpandedOrCollapsed(self, libraryItem:QtWidgets.QTreeWidgetItem):
|
|
if type(libraryItem) is GuiLibrary : #just in case
|
|
api.session.guiSharedDataToSave["libraryIsExpanded"][libraryItem.id] = libraryItem.isExpanded()
|
|
|
|
|
|
def currentTreeItemChanged(self, currentTreeItem:QtWidgets.QTreeWidgetItem):
|
|
"""
|
|
Program wide GUI-only callback from
|
|
widget.currentItemChanged->mainWindow.currentTreeItemChanged. We set the currentItem
|
|
ourselves, so we need to block our signals to avoid recursion.
|
|
|
|
Only one item can be selected at a time.
|
|
|
|
The currentTreeItem we receive is not a global instance but from a widget different to ours.
|
|
We need to find our local version of the same instrument/library/idKey first.
|
|
"""
|
|
if not self.guiLibraries or not self.guiInstruments:
|
|
#"Show Only Loaded" but no instruments are.
|
|
#Or Non-Nested view without libraries
|
|
return
|
|
|
|
self.treeWidget.blockSignals(True)
|
|
|
|
isLibrary = type(currentTreeItem) is GuiLibrary
|
|
idKey = currentTreeItem.idKey
|
|
|
|
if isLibrary and idKey in self.guiLibraries:
|
|
assert idKey in self.guiLibraries, (idKey, self.guiLibraries)
|
|
item = self.guiLibraries[idKey]
|
|
else:
|
|
assert idKey in self.guiInstruments, (idKey, self.guiInstruments)
|
|
item = self.guiInstruments[idKey]
|
|
|
|
self.treeWidget.setCurrentItem(item) #This will work at first, but as soon as the tab instr/favorites is changed this will jump back to a wrong item (first of the group). We have a tabWidget.changed signal in the main window to reset this
|
|
self.treeWidget.blockSignals(False)
|
|
|
|
def itemDoubleClicked(self, item:QtWidgets.QTreeWidgetItem, column:int):
|
|
"""This chooses the auditioner. Callbacks are handled in the auditioner widget itself"""
|
|
if type(item) is GuiInstrument:
|
|
api.auditionerInstrument(item.idKey)
|
|
|
|
def isNested(self)->bool:
|
|
return self.parentMainWindow.ui.actionFlatNested.isChecked()
|
|
|
|
def isShowOnlyLoadedInstruments(self)->bool:
|
|
return self.parentMainWindow.ui.actionShowOnlyLoadedInstruments.isChecked()
|
|
|
|
def buildTree(self, data:dict):
|
|
"""
|
|
Create the tree. Can be called multiple times and it will re-create itself destructively.
|
|
If you call it with data, once at program start, data will get cached. If you call
|
|
with data=None it will used the cached variant.
|
|
|
|
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',
|
|
'idKey': ('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',
|
|
'idKey': ('0', '1'),
|
|
'license': '',
|
|
'name': 'Square Wave',
|
|
'tags': ['square', 'basic'],
|
|
'variants': ['Square.sfz', 'Square With 5th Slide.sfz'],
|
|
'vendor': ''},
|
|
'2': {'group': 'brass',
|
|
'id': '2',
|
|
'idKey': ('0', '2'),
|
|
'license': '',
|
|
'name': 'Saw Wave',
|
|
'tags': ['saw', 'basic'],
|
|
'variants': ['Saw.sfz', 'Saw With 5th Slide.sfz'],
|
|
'vendor': ''},
|
|
'3': {'group': '',
|
|
'id': '3',
|
|
'idKey': ('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'}}}
|
|
"""
|
|
#Reset everything except our cached data.
|
|
self.treeWidget.clear() #will delete the C++ objects. We need to delete the PyQt objects ourselves, like so:
|
|
self.guiLibraries = {} # idKey : GuiLibrary idKey is a tuple with second value -1, which would be the instrument.
|
|
self.guiInstruments = {} # idKey : GuiInstrument
|
|
|
|
if data:
|
|
self._cachedData = data
|
|
else:
|
|
assert self._cachedData
|
|
data = self._cachedData
|
|
|
|
|
|
nested = self.isNested()
|
|
showOnlyLoadedInstruments = self.isShowOnlyLoadedInstruments()
|
|
|
|
for libraryId, libraryDict in data.items():
|
|
|
|
if nested:
|
|
parentLibraryWidget = GuiLibrary(parentTreeController=self, libraryDict=libraryDict["library"])
|
|
self.guiLibraries[(libraryId, -1)] = parentLibraryWidget #-1 marks the library but keeps it a tuple.
|
|
self.treeWidget.addTopLevelItem(parentLibraryWidget)
|
|
if libraryId in api.session.guiSharedDataToSave["libraryIsExpanded"]:
|
|
parentLibraryWidget.setExpanded(api.session.guiSharedDataToSave["libraryIsExpanded"][libraryId]) #only possible after gi.init() was done and item inserted.
|
|
#parentLibraryWidget.setHidden(True) #only possible after insert
|
|
|
|
atLeastOneVisible = False
|
|
for instrumentdId, instrumentDict in libraryDict.items():
|
|
if instrumentdId == "library":
|
|
#Top level item was already created. Ignore here.
|
|
continue
|
|
|
|
if showOnlyLoadedInstruments and instrumentDict["idKey"] in self._cachedLastInstrumentStatus and not self._cachedLastInstrumentStatus[instrumentDict["idKey"]]["state"]: #don't create if we only want to see enabled instrument
|
|
instrumentVisible = False
|
|
elif showOnlyLoadedInstruments and not instrumentDict["idKey"] in self._cachedLastInstrumentStatus and not instrumentDict["status"]["state"]:
|
|
#Not cached yet. This is only good for "load from file". The instruments are not in _cachedLastInstrumentStatus yet
|
|
instrumentVisible = False
|
|
else:
|
|
instrumentVisible = True
|
|
atLeastOneVisible = True
|
|
|
|
gi = GuiInstrument(parentTreeController=self, instrumentDict=instrumentDict)
|
|
if nested:
|
|
parentLibraryWidget.addChild(gi)
|
|
else:
|
|
self.treeWidget.addTopLevelItem(gi)
|
|
gi.injectWidgets() #only possible after gi.init() was done and item inserted.
|
|
self.guiInstruments[instrumentDict["idKey"]] = gi
|
|
if instrumentDict["idKey"] in self._cachedLastInstrumentStatus:
|
|
gi.updateStatus(self._cachedLastInstrumentStatus[instrumentDict["idKey"]])
|
|
gi.setHidden(not instrumentVisible)
|
|
|
|
#If no instruments were added maybe the lib needs hiding because it is empty
|
|
if showOnlyLoadedInstruments and nested and not atLeastOneVisible:
|
|
parentLibraryWidget.setHidden(True)
|
|
|
|
|
|
self._adjustColumnSize()
|
|
|
|
|
|
def showOnlyLoadedInstruments(self, state:bool):
|
|
"""The logic is backwards. We receive state=True if instruments should be hidden"""
|
|
self.buildTree(data=None) #uses the menu state for everything except data
|
|
|
|
def setAllExpanded(self, state:bool):
|
|
"""We do not use the qt function collapseAll and expandAll because they do not trigger
|
|
the signal"""
|
|
if self.isNested():
|
|
for idKey, guiLib in self.guiLibraries.items():
|
|
guiLib.setExpanded(state)
|
|
|
|
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):
|
|
self.parentMainWindow.qtApp.restoreOverrideCursor() #Sometimes the instrument was loaded with a cursor animation
|
|
if instrumentStatus["idKey"] in self.guiInstruments:
|
|
gi = self.guiInstruments[instrumentStatus["idKey"]]
|
|
gi.updateStatus(instrumentStatus)
|
|
self._adjustColumnSize()
|
|
|
|
#We also cache the last status, as we cache the initial data. This way we can delete and recreate TreeItems without requesting new status data from the engine
|
|
self._cachedLastInstrumentStatus[instrumentStatus["idKey"]] = instrumentStatus
|
|
|
|
def react_startLoadingSamples(self, idKey:tuple):
|
|
"""Will be overriden by instrument status change / variant chosen"""
|
|
self.parentMainWindow.qtApp.setOverrideCursor(QtCore.Qt.WaitCursor) #reset in self.react_instrumentStatusChanged
|
|
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 and cursor
|
|
|
|
def react_instrumentMidiNoteOnActivity(self, idKey:tuple, pitch:int, velocity:int):
|
|
#First figure out which instrument has activity
|
|
if idKey in self.guiInstruments:
|
|
gi = self.guiInstruments[idKey]
|
|
gi.activity() #We only do a quick flash here. No need for velocity, pitch or note-off
|
|
|
|
|
|
def contextMenu(self, qpoint): #strange that this is not an event but a qpoint
|
|
item = self.treeWidget.itemAt(qpoint)
|
|
if not item:
|
|
return
|
|
elif type(item) is GuiLibrary:
|
|
item.contextMenu(qpoint)
|
|
elif type(item) is GuiInstrument and not item.state:
|
|
return
|
|
else: #GuiInstrument and item.state
|
|
assert item.state
|
|
externalPort = nestedJackPortsMenu() #returns None or clientAndPortString
|
|
if externalPort: #None or ClientPortString
|
|
if externalPort == -1:
|
|
externalPort = ""
|
|
api.connectInstrumentPort(item.idKey, externalPort)
|
|
|
|
|
|
|
|
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.id = libraryDict["id"]
|
|
self.idKey = (libraryDict["id"], -1) #fake it for compatibility. -1 means library
|
|
|
|
self.name = libraryDict["name"]
|
|
|
|
#No dynamic data here. Everything gets created once.
|
|
#self.setText(COLUMNS.index("idKey"), str(libraryDict["id"]).zfill(leadingZeroesForZfill))
|
|
self.setData(COLUMNS.index("idKey"), 0, int(libraryDict["id"])) #set data allows sorting by actual numbers. 0 is the data role, which is just "display text".
|
|
self.setText(COLUMNS.index("name"), str(libraryDict["name"]))
|
|
self.setText(COLUMNS.index("tags"), str(libraryDict["description"])[:42]+"…")
|
|
|
|
#Hack the row height through an unused column.
|
|
#self.setText(COLUMNS.index("loaded"), "")
|
|
|
|
#We cannot call setExpanded here. The item must first be inserted into the parent tree.
|
|
#We placed this call in InstrumentTreeController.buildTree
|
|
#self.setExpanded(False)
|
|
|
|
icon = parentTreeController.parentMainWindow.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DirIcon"))
|
|
self.setIcon(COLUMNS.index("name"), icon)
|
|
|
|
def contextMenu(self, qpoint):
|
|
"""This isn't the qt function, but we call this from self.parentTreeController.
|
|
Logically it makes sense to have it here though"""
|
|
menu = QtWidgets.QMenu()
|
|
|
|
listOfLabelsAndFunctions = [
|
|
(QtCore.QCoreApplication.translate("GuiLibraryContextMenu", "Load whole Library"), lambda: api.loadLibraryInstrumentSamples(self.id)),
|
|
(QtCore.QCoreApplication.translate("GuiLibraryContextMenu", "Unload whole Library"), lambda: api.unloadLibraryInstrumentSamples(self.id)),
|
|
]
|
|
|
|
for text, function in listOfLabelsAndFunctions:
|
|
if function is None:
|
|
l = QtWidgets.QLabel(text)
|
|
l.setAlignment(QtCore.Qt.AlignCenter)
|
|
a = QtWidgets.QWidgetAction(menu)
|
|
a.setDefaultWidget(l)
|
|
menu.addAction(a)
|
|
else:
|
|
a = QtWidgets.QAction(text, menu)
|
|
menu.addAction(a)
|
|
a.triggered.connect(function)
|
|
|
|
pos = QtGui.QCursor.pos()
|
|
pos.setY(pos.y() + 5)
|
|
menu.exec_(pos)
|
|
|
|
|
|
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.
|
|
"""
|
|
|
|
def __init__(self, parentTreeController, instrumentDict):
|
|
self.parentTreeController = parentTreeController
|
|
self.idKey = instrumentDict["idKey"]
|
|
|
|
#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("idKey"), QtCore.Qt.AlignHCenter)
|
|
|
|
self.state = None #by self.update...
|
|
self.instrumentDict = None
|
|
self._writeColumns(instrumentDict)
|
|
|
|
self.mutedMixSendStylesheet = """
|
|
QSlider::handle:horizontal {
|
|
background: #5c0000;
|
|
border-radius: 4px;
|
|
height: 8px;
|
|
}
|
|
"""
|
|
|
|
self.mixSendDial = QtWidgets.QSlider(QtCore.Qt.Horizontal)
|
|
self.mixSendDial.setMaximumSize(QtCore.QSize(48, 16))
|
|
self.mixSendDial.setPageStep(3)
|
|
self.mixSendDial.setMaximum(0)
|
|
self.mixSendDial.setMinimum(-21)
|
|
self.mixSendDial.setValue(-3)
|
|
self.mixSendDial.valueChanged.connect(self._sendVolumeChangeToEngine)
|
|
self.mixSendDial.enterEvent = lambda ev: self.parentTreeController.parentMainWindow.statusBar().showMessage((QtCore.QCoreApplication.translate("Instrument", "Use mousewheel to change the instruments mixSend for the stereo mixer ouput. Right click to (un)mute mixer-send.")))
|
|
self.mixSendDial.leaveEvent = lambda ev: self.parentTreeController.parentMainWindow.statusBar().showMessage("")
|
|
self.leaveEvent = lambda ev: self.parentTreeController.parentMainWindow.statusBar().showMessage("")
|
|
self.mixSendDial.contextMenuEvent = self._mixSendDialContextMenuEvent
|
|
self.mixSendDial.setEnabled(False) #default is off, so we don't send mixSend changes for an unloaded instrument
|
|
|
|
self.toggleSwitch = FancySwitch(track_radius=8,thumb_radius=7) #radius is the actual size, not the rounded corners.
|
|
#self.toggleSwitch.toggled.connect(lambda c: print('toggled', c)) #triggered by engine callback as well
|
|
self.toggleSwitch.setAutoFillBackground(True) #otherwise conflicts with setItemWidget
|
|
self.toggleSwitch.clicked.connect(self.instrumentSwitchOnViaGui)
|
|
self.toggleSwitch.leaveEvent = lambda ev: self.parentTreeController.parentMainWindow.statusBar().showMessage("")
|
|
#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
|
|
|
|
#icon = parentTreeController.parentMainWindow.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ComputerIcon"))
|
|
#px = QtGui.QPixmap(32,32) #TODO: get size from standard icon above. but how? Apparently it does not matter how big this is.
|
|
#px.fill(QtGui.QColor(255,255,255,0)) #transparent icon)
|
|
#icon = QtGui.QIcon(px)
|
|
#self.setIcon(COLUMNS.index("name"), icon)
|
|
|
|
#Create icons for midi in status
|
|
on = QtGui.QPixmap(32,32)
|
|
oncolor = QtGui.QColor("cyan")
|
|
on.fill(oncolor)
|
|
self.onIcon = QtGui.QIcon(on)
|
|
|
|
off = QtGui.QPixmap(32,32)
|
|
#offcolor = QtGui.QColor(50, 50, 50) #dark grey
|
|
offcolor = QtGui.QColor(255,255,255,0) #transparent
|
|
off.fill(offcolor)
|
|
self.offIcon = QtGui.QIcon(off)
|
|
|
|
self.setIcon(COLUMNS.index("name"), self.offIcon)
|
|
|
|
self.midiActiveFlag = False #midi indicator
|
|
api.session.eventLoop.verySlowConnect(self._activityOff) #this is always on, even if no samples loaded. The auditioner sends activeOn, even if state==False
|
|
|
|
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"
|
|
|
|
We use this to count, across program runs, how many times this instrument was activated
|
|
and present it as favourites. The engine does not know about this.
|
|
Loading states from save files, "load all" and other algorithmic loaders do not contribute
|
|
to this counting.
|
|
"""
|
|
if state:
|
|
api.loadInstrumentSamples(self.idKey)
|
|
self.parentTreeController.parentMainWindow.favoriteInstrument(self.idKey)
|
|
else:
|
|
api.unloadInstrumentSamples(self.idKey)
|
|
|
|
def updateStatus(self, instrumentStatus:dict):
|
|
#Before we set the state permanently we use the opportunity to see if this is program state (state == None) or unloading
|
|
self.cachedInstrumentStatus = instrumentStatus
|
|
firstLoad = self.state is None
|
|
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")) #either "" or a variant
|
|
self.state = instrumentStatus["state"] #is no bool and not None.
|
|
self.toggleSwitch.setChecked(instrumentStatus["state"])
|
|
self.mixSendDial.setStyleSheet("")
|
|
self.mixSendDial.setUpdatesEnabled(True)
|
|
|
|
if self.state:
|
|
#either reload or load for the first time
|
|
self.mixSendDial.setEnabled(True)
|
|
#Qslider can only have integer values. mixerLevel" is float.
|
|
#We simply drop the float part, it doesn't matter for our coarse in-house mixing.
|
|
self.mixSendDial.setValue(int(instrumentStatus["mixerLevel"]))
|
|
if instrumentStatus["mixerEnabled"]:
|
|
muteText = ""
|
|
else:
|
|
muteText = QtCore.QCoreApplication.translate("Instrument", "[Muted]")
|
|
self.mixSendDial.setStyleSheet(self.mutedMixSendStylesheet)
|
|
|
|
self.parentTreeController.parentMainWindow.statusBar().showMessage((QtCore.QCoreApplication.translate("Instrument", "{} send to mix volume: {} {}".format(self.instrumentDict["name"], instrumentStatus["mixerLevel"], muteText))))
|
|
#api.session.eventLoop.verySlowConnect(self._activityOff) #this is always on, even if not loaded. The auditioner sends activeOn, even if state==False
|
|
elif not firstLoad: #and not self.state
|
|
#the instrument was once loaded and is currently connected
|
|
pass
|
|
#api.session.eventLoop.verySlowDisconnect(self._activityOff)
|
|
|
|
|
|
if not self.state: #in any not-case
|
|
self.mixSendDial.setEnabled(False)
|
|
self.mixSendDial.setUpdatesEnabled(False) #this is a hack to make the widget disappear. Because hiding a QTreeWidgetItem does not work.
|
|
|
|
self.parentTreeController.parentMainWindow.qtApp.processEvents() #actually show the new state, even if mass unloading (which did not work before. But loading worked!)
|
|
|
|
|
|
def _mixSendDialContextMenuEvent(self, event):
|
|
if self.cachedInstrumentStatus["mixerEnabled"]:
|
|
mixerMuteText = QtCore.QCoreApplication.translate("InstrumentMixerLevelContextMenu", "Mute/Disable Mixer-Send for {}".format(self.instrumentDict["name"]))
|
|
mixerMuteFunc = lambda: api.setInstrumentMixerEnabled(self.idKey, False)
|
|
else:
|
|
mixerMuteText = QtCore.QCoreApplication.translate("InstrumentMixerLevelContextMenu", "Unmute/Enable Mixer-Send for {}".format(self.instrumentDict["name"]))
|
|
mixerMuteFunc = lambda: api.setInstrumentMixerEnabled(self.idKey, True)
|
|
|
|
listOfLabelsAndFunctions = [
|
|
(mixerMuteText, mixerMuteFunc),
|
|
]
|
|
|
|
menu = QtWidgets.QMenu()
|
|
for text, function in listOfLabelsAndFunctions:
|
|
if function is None:
|
|
l = QtWidgets.QLabel(text)
|
|
l.setAlignment(QtCore.Qt.AlignCenter)
|
|
a = QtWidgets.QWidgetAction(menu)
|
|
a.setDefaultWidget(l)
|
|
menu.addAction(a)
|
|
else:
|
|
a = QtWidgets.QAction(text, menu)
|
|
menu.addAction(a)
|
|
a.triggered.connect(function)
|
|
|
|
pos = QtGui.QCursor.pos()
|
|
pos.setY(pos.y() + 5)
|
|
menu.exec_(pos)
|
|
|
|
def injectWidgets(self):
|
|
"""Call this after the item was added to the tree. Widgets must be inserted after the Item
|
|
was created"""
|
|
stateColumnIndex = self.columns.index("state")
|
|
self.parentTreeController.treeWidget.setItemWidget(self, stateColumnIndex, self.toggleSwitch)
|
|
|
|
mixSendColumnIndex = self.columns.index("mixSend")
|
|
self.parentTreeController.treeWidget.setItemWidget(self, mixSendColumnIndex, self.mixSendDial)
|
|
|
|
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" or key == "mixSend":
|
|
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 == "idKey": #tuple
|
|
libId, instrId = instrumentDict[key]
|
|
zeros = int(instrumentDict["instrumentsInLibraryCount"]/10)+1
|
|
instIdZFilled = str(instrId).zfill(zeros)
|
|
if self.parentTreeController.isNested():
|
|
self.setText(index, instIdZFilled)
|
|
else: #full id
|
|
#self.setText(index, f"{libId}-{str(instrId).zfill(zeros)}")
|
|
self.setData(index, 0, float(str(libId) + "." + instIdZFilled)) #0 is the data role, just standard display text. We combine both IDs to a float number for sorting. If used with setData instead of setText Qt will know how to sort 11 before 1000
|
|
|
|
|
|
|
|
"""
|
|
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 _activityOff(self):
|
|
"""Called by a timer"""
|
|
if self.midiActiveFlag:
|
|
self.midiActiveFlag = False
|
|
self.setIcon(COLUMNS.index("name"), self.offIcon)
|
|
|
|
def activity(self):
|
|
"""Show midi note ons as flashing light."""
|
|
self.midiActiveFlag = True
|
|
self.setIcon(COLUMNS.index("name"), self.onIcon)
|
|
|
|
def _sendVolumeChangeToEngine(self, newValue):
|
|
self.mixSendDial.blockSignals(True)
|
|
api.setInstrumentMixerVolume(self.idKey, newValue)
|
|
self.mixSendDial.blockSignals(False)
|
|
|