Browse Source

More code. Hide unused features for now, to prepare for a public alpha relase

master
Nils 3 years ago
parent
commit
64f3a44024
  1. 18
      engine/api.py
  2. 15
      engine/instrument.py
  3. 82
      engine/main.py
  4. 107
      engine/resources/000 - Default.ini
  5. BIN
      engine/resources/000 - Default.tar
  6. 13
      qtgui/auditioner.py
  7. 93
      qtgui/chooseDownloadDirectory.py
  8. 71
      qtgui/designer/chooseDownloadDirectory.py
  9. 104
      qtgui/designer/chooseDownloadDirectory.ui
  10. 247
      qtgui/designer/mainwindow.bak
  11. 11
      qtgui/designer/mainwindow.py
  12. 13
      qtgui/designer/mainwindow.ui
  13. 100
      qtgui/instrument.py
  14. 28
      qtgui/mainwindow.py
  15. 5
      template/qtgui/helper.py
  16. 8
      template/qtgui/mainwindow.py

18
engine/api.py

@ -116,7 +116,9 @@ from template.engine.api import callbacks
_templateStartEngine = startEngine
def startEngine(nsmClient):
def startEngine(nsmClient, additionalData):
session.data.parseAndLoadInstrumentLibraries(additionalData["baseSamplePath"])
_templateStartEngine(nsmClient) #loads save files or creates empty structure.
#Send initial Callbacks to create the first GUI state.
@ -178,6 +180,20 @@ def chooseVariantByIndex(idkey:tuple, variantIndex:int):
callbacks._instrumentStatusChanged(*instrument.idKey)
callbacks._dataChanged
def setInstrumentMixerVolume(idkey:tuple, value:float):
"""From 0 to -21.
Default is -3.0 """
instrument = Instrument.allInstruments[idkey]
instrument.mixerLevel = value
libraryId, instrumentId = idkey
callbacks._instrumentStatusChanged(libraryId, instrumentId)
def setInstrumentMixerEnabled(idkey:tuple, state:bool):
instrument = Instrument.allInstruments[idkey]
instrument.setMixerEnabled(state)
libraryId, instrumentId = idkey
callbacks._instrumentStatusChanged(libraryId, instrumentId)
def auditionerInstrument(idkey:tuple):
"""Load an indendepent instance of an instrument into the auditioner port"""
libraryId, instrumentId = idkey

15
engine/instrument.py

@ -94,9 +94,9 @@ class Instrument(object):
self.tarFilePath = tarFilePath
self.libraryId = libraryId
self.startVariantSfzFilename = startVariantSfzFilename
self.idKey = (self.libraryId, self.metadata["id"])
self.idKey = (self.libraryId, int(self.metadata["id"]))
Instrument.allInstruments[self.idKey] = self
self.id = self.metadata["id"]
self.id = int(self.metadata["id"])
self.enabled = False #At startup no samples and no jack-ports. But we already have all metadata ready so a GUI can build a database and offer Auditioner choices.
self.mixerEnabled = None #not only is the mixer disabled, but it is unavailable.
@ -119,13 +119,14 @@ class Instrument(object):
self.currentVariant:str = "" #This is the currently loaded variant. Only set after actual loading samples. That means it is "" even from a savefile and only set later.
#We could call self.loadSamples() now, but we delay that for the user experience. See docstring.
def exportStatus(self)->dict:
"""The call-often function to get the instrument status. Includes only data that can
actually change during runtime."""
result = {}
#Static ids
result["id"] = self.metadata["id"]
result["id"] = int(self.metadata["id"])
result["id-key"] = self.idKey #redundancy for convenience.
#Dynamic data
@ -133,7 +134,7 @@ class Instrument(object):
result["currentVariantWithoutSfzExtension"] = self.currentVariant.rstrip(".sfz") if self.currentVariant else ""# str
result["state"] = self.enabled #bool
result["mixerEnabled"] = self.mixerEnabled #bool
#result["mixerLevel"] = self.mixerLevel #float.
result["mixerLevel"] = self.mixerLevel #float.
return result
@ -147,7 +148,7 @@ class Instrument(object):
parentMetadata = self.parentLibrary.config["library"]
result = {}
result["id"] = self.metadata["id"] #int
result["id"] = int(self.metadata["id"]) #int
result["id-key"] = self.idKey # tuple (int, int) redundancy for convenience.
result["name"] = self.metadata["name"] #str
result["description"] = self.metadata["description"] #str
@ -272,6 +273,8 @@ class Instrument(object):
#self.midiProcessor.notePrinter(True)
self.parentLibrary.parentData.parentSession.eventLoop.slowConnect(self.midiProcessor.processEvents)
self.parentLibrary.parentData.updateJackMetadataSorting()
@property
def mixerLevel(self)->float:
if self.enabled:
@ -356,6 +359,8 @@ class Instrument(object):
self.midiProcessor = None
self.mixerEnabled = None #not only is the mixer disabled, but it is unavailable.
self.parentLibrary.parentData.updateJackMetadataSorting()
#Save
def serialize(self)->dict:
return {

82
engine/main.py

@ -65,19 +65,36 @@ class Data(TemplateData):
self.cachedSerializedDataForStartEngine = None
def parseAndLoadInstrumentLibraries(self, baseSamplePath):
"""Called once by api.startEngine, which receives the global sample path from
the GUI"""
#Parse all tar libraries and load them all. ALL OF THEM!
#for each library in ...
self.libraries = {} # libraryId:int : Library-object
basePath = pathlib.Path("/home/nils/samples/Tembro/out/") #TODO: replace with system-finder /home/xy or /usr/local/share or /usr/share etc.
s = pathlib.Path(PATHS["share"])
defaultLibraryPath = s.joinpath("000 - Default.tar")
logger.info(f"Loading default test library from {defaultLibraryPath}")
defaultLib = Library(parentData=self, tarFilePath=defaultLibraryPath)
self.libraries[defaultLib.id] = defaultLib
#basePath = pathlib.Path("/home/nils/samples/Tembro/out/") #TODO: replace with system-finder /home/xy or /usr/local/share or /usr/share etc.
basePath = pathlib.Path(baseSamplePath) #self.baseSamplePath is injected by api.startEngine.
logger.info(f"Start loading samples from {baseSamplePath}")
for f in basePath.glob('*.tar'):
if f.is_file() and f.suffix == ".tar":
lib = Library(parentData=self, tarFilePath=f)
self.libraries[lib.id] = lib
logger.info(f"Finished loading samples from {baseSamplePath}")
if not self.libraries:
logger.error("There were no sample libraries to parse! This is correct on the first run, since you still need to choose a sample directory.")
self.instrumentMidiNoteOnActivity = None # the api will inject a callback function here which takes (libId, instrId) as parameter to indicate midi noteOn activity for non-critical information like a GUI LED blinking. The instruments individiual midiprocessor will call this as a parent-call.
self._createGlobalPorts() #in its own function for readability
self._createCachedJackMetadataSorting()
def _createGlobalPorts(self):
"""Create two mixer ports, for stereo. Each instrument will not only create their own jack
@ -109,6 +126,54 @@ class Data(TemplateData):
return result
def _createCachedJackMetadataSorting(self):
"""Calculate once, per programstart, what port order we had if all instruments were loaded.
In reality they are not, but we can still use this cache instead of dynamically creating
a port order.
Needs to be called after parsing the tar/ini files."""
#order = {portName:index for index, portName in enumerate(track.sequencerInterface.cboxPortName() for track in self.tracks if track.sequencerInterface.cboxMidiOutUuid)}
highestLibId = max(self.libraries.keys())
highestInstrId = max(instId for libId, instId in Instrument.allInstruments.keys())
s = set()
clientName = cbox.JackIO.status().client_name
order = {}
for (libraryId, instrumentId), instr in Instrument.allInstruments.items():
L = clientName + ":" + instr.midiInputPortName + "_L"
R = clientName + ":" + instr.midiInputPortName + "_R"
lnum = highestLibId * libraryId + instrumentId * 3 # * 3 to make room for stereo pairs and midi. #TODO: If we get non-stereo pairs this must be solved
assert not lnum in s, lnum
assert not lnum+1 in s, lnum+1
assert not lnum+2 in s, lnum+2
s.add(lnum)
s.add(lnum+1)
s.add(lnum+2)
order[L] = (lnum, instr)
order[R] = (lnum+1, instr)
order[clientName + ":" + instr.midiInputPortName] = (lnum+2, instr) #midi port
self._cachedJackMedataPortOrder = order
def updateJackMetadataSorting(self):
"""
Tell cbox to reorder the tracks by metadata.
We need this everytime we enable/disable an instrument which adds/removes the
jack ports.
Luckily our data never changes. We can just prepare one order, cache it, filter it
and send that again and again.
"""
cleanedOrder = { fullportname : index for fullportname, (index, instrObj) in self._cachedJackMedataPortOrder.items() if instrObj.enabled}
try:
cbox.JackIO.Metadata.set_all_port_order(cleanedOrder) #wants a dict with {complete jack portname : sortIndex}
except Exception as e: #No Jack Meta Data or Error with ports.
logger.error(e)
#Save / Load
def serialize(self)->dict:
return {
@ -118,7 +183,7 @@ class Data(TemplateData):
@classmethod
def instanceFromSerializedData(cls, parentSession, serializedData):
"""As an experiment we try to load everything from this function alone and not create a
function hirarchy. Save is in a hirarchy though.
function hirarchy. Save is in a hierarchy though.
This differs from other LSS programs that most data is just static stuff.
Instead we delay loading until everything is setup and just deposit saved data here
@ -196,15 +261,16 @@ class Library(object):
self.config = configparser.ConfigParser()
self.config.read(iniName)
self.id = self.config["library"]["id"]
self.id = int(self.config["library"]["id"])
instrumentSections = self.config.sections()
instrumentSections.remove("library")
instrumentsInLibraryCount = len(instrumentSections)
self.instruments = {} # instrId : Instrument()
for iniSection in instrumentSections:
instrObj = Instrument(self, self.config["library"]["id"], self.config[iniSection], tarFilePath)
instrId = int(self.config[iniSection]["id"])
instrObj = Instrument(self, self.id, self.config[iniSection], tarFilePath)
instrObj.instrumentsInLibraryCount = instrumentsInLibraryCount
self.instruments[self.config[iniSection]["id"]] = instrObj
self.instruments[instrId] = instrObj
#At a later point Instrument.loadSamples() must be called. This is done in the API.
def exportMetadata(self)->dict:
@ -216,9 +282,9 @@ class Library(object):
result["library"] = libDict
#Explicit is better than implicit
assert self.config["library"]["id"] == self.id, (self.config["library"]["id"], self.id)
assert int(self.config["library"]["id"]) == self.id, (int(self.config["library"]["id"]), self.id)
libDict["tarFilePath"] = self.tarFilePath
libDict["id"] = self.config["library"]["id"]
libDict["id"] = int(self.config["library"]["id"])
libDict["name"] = self.config["library"]["name"]
libDict["description"] = self.config["library"]["description"]
libDict["license"] = self.config["library"]["license"]

107
engine/resources/000 - Default.ini

@ -0,0 +1,107 @@
[library]
;All parameters must be set.
;Library ID is unique across all Tembro Library files
id=0
name=Tembro Defaul Instrument
description=This uses the sfz-Synthesizer features, and not samples. If this is the only instrument
you see you have not (correctly) downloaded the sample files and set the correct path via
the Edit menu.
license=https://creativecommons.org/publicdomain/zero/1.0/
vendor=Hilbricht Nils 2021, Laborejo Software Suite https://www.laborejo.org info@laborejo.org
;;;;;;;;;;;;;;;;;;;
;;;;Instruments;;;;
;;;;;;;;;;;;;;;;;;;
;Each instrument creates its own JACK midi port and audio output ports.
;Instrument [section] names have no meaning except to differentiate them from "library".
;The explict id (see below) is the permanent instrument identifier.
[sine]
;Instrument parameters that are mandatory:
;Instrument ID is unique within this library file
id=0
;The pretty name for the whole instrument. If you cannot find a name that describes all variants
;(see below) that is a sign that the variants are not the same instrument.
name=Sine Wave
;Variants are case sensitive and comma separated. Sorry, no commas in sfz file names here.
;No space-padding. Spaces in filenames are allowed.
;The order and naming of these files MUST remain permanently the same. This is most likely a version history.
variants=Sine.sfz
;Which variant shall be loaded when no save file exists. When updated versions are created this may change.
defaultVariant=Sine.sfz
description=Sine wave synthesizer.
;Tags are comma separated keywords for search and filtering. Do not create arbitrary tags. They are premade.
tags=sine
;Instrument parameters that are optional. With explanation why:
;Additional Vendor notes. They are specific to this instrument.
;Use them to add additional persons or explain differences to
;the library author information provided above
;UNCOMMENT vendor=Test entry to provide more vendor information
;An instrument license entry will override the library one. The license will be shown in each
;instruments GUI entry. Either the default one or *only* the license one. This is different to the
;author entry, where additional information is just appended.
;UNCOMMENT license=https://unlicense.org/
;Group. While tags are a global way to group across all libraries a group is a one-level structure
;within a library. Instruments without a group are top-level instruments (within the lib).
;Intended mainly for common groups like orchstra-sections (brass, strings etc.)
group=synthesizer
;From here on just the basics:
[saw]
id=1
name=Saw Wave
variants=Saw.sfz
defaultVariant=Saw.sfz
description=Saw wave synthesizer
tags=saw
group=synthesizer
[square]
id=2
name=Square Wave
variants=Square.sfz
defaultVariant=Square.sfz
description=Square wave synthesizer
tags=Square
group=synthesizer
[triangle]
id=3
name=Triangle Wave
variants=Triangle.sfz
defaultVariant=Triangle.sfz
description=Triangle wave synthesizer
tags=Triangle
group=synthesizer
[noise]
id=4
name=Noise
variants=Noise.sfz
defaultVariant=Noise.sfz
description=Noise from a synthesizer
tags=Noise
group=synthesizer
[silence]
id=5
name=Silence
variants=Silence.sfz
defaultVariant=Silence.sfz
description=Literally silence, but it is synthesized nevertheless, and goes through the whole audio-chain in the program.
If you find a usecase for this please send an email to info@laborejo.org :)
tags=Silence
group=synthesizer

BIN
engine/resources/000 - Default.tar

Binary file not shown.

13
qtgui/auditioner.py

@ -40,7 +40,7 @@ class AuditionerMidiInputComboController(object):
self.volumeDial.setMaximum(0)
self.volumeDial.setMinimum(-21)
self.volumeDial.valueChanged.connect(self._sendVolumeChangeToEngine)
#self.volumeDial.setAttribute(QtCore.Qt.WA_Hover)
self.volumeDial.enterEvent = lambda ev: self.parentMainWindow.statusBar().showMessage((QtCore.QCoreApplication.translate("Auditioner", "Use mousewheel to change the Auditioner Volume")))
self.volumeDial.leaveEvent = lambda ev: self.parentMainWindow.statusBar().showMessage("")
self.wholePanel = parentMainWindow.ui.auditionerWidget
self.currentInstrumentLabel = parentMainWindow.ui.auditionerCurrentInstrument_label
@ -60,11 +60,6 @@ class AuditionerMidiInputComboController(object):
api.callbacks.auditionerInstrumentChanged.append(self.callback_auditionerInstrumentChanged)
api.callbacks.auditionerVolumeChanged.append(self.callback__auditionerVolumeChanged)
def _sendVolumeChangeToEngine(self, newValue):
self.volumeDial.blockSignals(True)
api.setAuditionerVolume(newValue)
self.volumeDial.blockSignals(False)
def callback_startLoadingAuditionerInstrument(self, idkey):
self.parentMainWindow.qtApp.setOverrideCursor(QtCore.Qt.WaitCursor) #reset in self.callback_auditionerInstrumentChanged
self.currentInstrumentLabel.setText(QtCore.QCoreApplication.translate("Auditioner", "…loading…"))
@ -80,6 +75,12 @@ class AuditionerMidiInputComboController(object):
self.volumeDial.setValue(value)
self.parentMainWindow.statusBar().showMessage(QtCore.QCoreApplication.translate("Auditioner", "Auditioner Volume: {}").format(value))
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
api.connectAuditionerPort(self.comboBox.currentText())

93
qtgui/chooseDownloadDirectory.py

@ -0,0 +1,93 @@
#! /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 Lib
from pathlib import Path
import os.path
from os import makedirs
#System Wide Modules
from PyQt5 import QtCore, QtWidgets, QtGui
#Template Moduiles
from .designer.chooseDownloadDirectory import Ui_ChooseDownloadDirectory
from .resources import * #has the translation
#Client Modules
from engine.config import * #imports METADATA
from qtgui.resources import * #Has the logo
class ChooseDownloadDirectory(QtWidgets.QDialog):
def __init__(self, qtApp):
super().__init__() #no parent, this is the top level window at this time.
self.qtApp = qtApp
self.setModal(True) #block until closed
self.ui = Ui_ChooseDownloadDirectory()
self.ui.setupUi(self)
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
if settings.contains("sampleDownloadDirectory"):
self.ui.pathComboBox.insertItem(0, settings.value("sampleDownloadDirectory", type=str))
else:
self.ui.pathComboBox.setCurrentText("")
self.ui.buttonBox.accepted.connect(self.accept)
self.ui.buttonBox.rejected.connect(self.reject)
self.ui.openFileDialogButton.setText("")
self.ui.openFileDialogButton.setIcon(self.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DialogOpenButton")))
self.ui.openFileDialogButton.clicked.connect(self.requestPathFromDialog)
self.exec()
def requestPathFromDialog(self):
if self.ui.pathComboBox.currentText() == "":
startPath = str(Path.home())
else:
startPath = self.ui.pathComboBox.currentText()
dirname = QtWidgets.QFileDialog.getExistingDirectory(self, QtCore.QCoreApplication.translate("ChooseDownloadDirectory", "Choose Download Directory"), startPath, QtWidgets.QFileDialog.ShowDirsOnly|QtWidgets.QFileDialog.DontResolveSymlinks)
if dirname:
self.ui.pathComboBox.setCurrentText(dirname)
def accept(self):
self.path = self.ui.pathComboBox.currentText() #easy abstraction so that the caller does not need to know our widget name
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
if not os.path.exists(self.path):
try:
makedirs(self.path)
except:
pass #file saving error logging is handled later
#There is no guarantee that the dir really exists. but at this point the user is on its own.
#It is allowed to use /dev/null after all
settings.setValue("sampleDownloadDirectory", self.path)
super().accept()
def reject(self):
self.path = None
super().reject()

71
qtgui/designer/chooseDownloadDirectory.py

@ -0,0 +1,71 @@
# -*- coding: utf-8 -*-
# Form implementation generated from reading ui file 'chooseDownloadDirectory.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_ChooseDownloadDirectory(object):
def setupUi(self, ChooseDownloadDirectory):
ChooseDownloadDirectory.setObjectName("ChooseDownloadDirectory")
ChooseDownloadDirectory.resize(410, 329)
self.verticalLayout = QtWidgets.QVBoxLayout(ChooseDownloadDirectory)
self.verticalLayout.setObjectName("verticalLayout")
self.layoutWidget = QtWidgets.QWidget(ChooseDownloadDirectory)
self.layoutWidget.setObjectName("layoutWidget")
self.formLayout = QtWidgets.QFormLayout(self.layoutWidget)
self.formLayout.setContentsMargins(-1, -1, -1, 0)
self.formLayout.setObjectName("formLayout")
self.label = QtWidgets.QLabel(self.layoutWidget)
self.label.setWordWrap(True)
self.label.setObjectName("label")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.label)
self.verticalLayout.addWidget(self.layoutWidget)
self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.pathComboBox = QtWidgets.QComboBox(ChooseDownloadDirectory)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.pathComboBox.sizePolicy().hasHeightForWidth())
self.pathComboBox.setSizePolicy(sizePolicy)
self.pathComboBox.setEditable(True)
self.pathComboBox.setObjectName("pathComboBox")
self.horizontalLayout_2.addWidget(self.pathComboBox)
self.openFileDialogButton = QtWidgets.QPushButton(ChooseDownloadDirectory)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.openFileDialogButton.sizePolicy().hasHeightForWidth())
self.openFileDialogButton.setSizePolicy(sizePolicy)
self.openFileDialogButton.setFlat(False)
self.openFileDialogButton.setObjectName("openFileDialogButton")
self.horizontalLayout_2.addWidget(self.openFileDialogButton)
self.verticalLayout.addLayout(self.horizontalLayout_2)
self.buttonBox = QtWidgets.QDialogButtonBox(ChooseDownloadDirectory)
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok)
self.buttonBox.setObjectName("buttonBox")
self.verticalLayout.addWidget(self.buttonBox)
self.widget_2 = QtWidgets.QWidget(ChooseDownloadDirectory)
self.widget_2.setObjectName("widget_2")
self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.widget_2)
self.horizontalLayout_4.setContentsMargins(0, 0, 0, 0)
self.horizontalLayout_4.setSpacing(0)
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
self.verticalLayout.addWidget(self.widget_2)
self.retranslateUi(ChooseDownloadDirectory)
QtCore.QMetaObject.connectSlotsByName(ChooseDownloadDirectory)
def retranslateUi(self, ChooseDownloadDirectory):
_translate = QtCore.QCoreApplication.translate
ChooseDownloadDirectory.setWindowTitle(_translate("ChooseDownloadDirectory", "Choose Session Directory"))
self.label.setText(_translate("ChooseDownloadDirectory", "<html><head/><body><p>Please choose a directory for your sample files. The location can be read-only and will be shared by all sessions. </p><p>At the moment you have to manually download the files and move them to this directory. An integrated downloader will be added to this program after its beta-phase. </p><p>Changing the directory requires a program restart. The sample libraries will only be scanned on program start as well.</p><p>The download URL is:</p><p align=\"center\"><a href=\"https://www.laborejo.org/tembro/instruments\"><span style=\" text-decoration: underline; color:#55ffff;\">https://www.laborejo.org/tembro/instruments</span></a></p></body></html>"))
self.openFileDialogButton.setText(_translate("ChooseDownloadDirectory", "Choose Directory"))

104
qtgui/designer/chooseDownloadDirectory.ui

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ChooseDownloadDirectory</class>
<widget class="QDialog" name="ChooseDownloadDirectory">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>410</width>
<height>329</height>
</rect>
</property>
<property name="windowTitle">
<string>Choose Session Directory</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QWidget" name="layoutWidget" native="true">
<layout class="QFormLayout" name="formLayout">
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="1">
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Please choose a directory for your sample files. The location can be read-only and will be shared by all sessions. &lt;/p&gt;&lt;p&gt;At the moment you have to manually download the files and move them to this directory. An integrated downloader will be added to this program after its beta-phase. &lt;/p&gt;&lt;p&gt;Changing the directory requires a program restart. The sample libraries will only be scanned on program start as well.&lt;/p&gt;&lt;p&gt;The download URL is:&lt;/p&gt;&lt;p align=&quot;center&quot;&gt;&lt;a href=&quot;https://www.laborejo.org/tembro/instruments&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#55ffff;&quot;&gt;https://www.laborejo.org/tembro/instruments&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QComboBox" name="pathComboBox">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="openFileDialogButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Choose Directory</string>
</property>
<property name="flat">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
<item>
<widget class="QWidget" name="widget_2" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

247
qtgui/designer/mainwindow.bak

@ -1,247 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1087</width>
<height>752</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QSplitter" name="splitter_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QGroupBox" name="search_groupBox">
<property name="title">
<string>Search</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QListWidget" name="search_listWidget"/>
</item>
</layout>
</widget>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QGroupBox" name="iinstruments_groupBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="title">
<string>Instruments</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="auditionerWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Auditioner MIDI Input</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="auditionerMidiInputComboBox">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="auditionerCurrentInstrument_label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="instruments_treeWidget">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<column>
<property name="text">
<string/>
</property>
</column>
<column>
<property name="text">
<string>ID</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Variant</string>
</property>
</column>
<column>
<property name="text">
<string>Tags</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QGroupBox" name="details_groupBox">
<property name="title">
<string>NamePlaceholder</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="details_scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>598</width>
<height>158</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="variant_label">
<property name="text">
<string>Variants</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="variants_comboBox"/>
</item>
<item row="1" column="1">
<widget class="QLabel" name="info_label">
<property name="text">
<string>TextLabel</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1087</width>
<height>20</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>

11
qtgui/designer/mainwindow.py

@ -102,7 +102,7 @@ class Ui_MainWindow(object):
self.scrollArea.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
self.scrollArea.setObjectName("scrollArea")
self.mixerAreaWidget = QtWidgets.QWidget()
self.mixerAreaWidget.setGeometry(QtCore.QRect(0, 0, 599, 447))
self.mixerAreaWidget.setGeometry(QtCore.QRect(0, 0, 626, 452))
self.mixerAreaWidget.setObjectName("mixerAreaWidget")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.mixerAreaWidget)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
@ -132,7 +132,7 @@ class Ui_MainWindow(object):
self.details_scrollArea.setWidgetResizable(True)
self.details_scrollArea.setObjectName("details_scrollArea")
self.scrollAreaWidgetContents = QtWidgets.QWidget()
self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 597, 155))
self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 624, 150))
self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
self.formLayout = QtWidgets.QFormLayout(self.scrollAreaWidgetContents)
self.formLayout.setObjectName("formLayout")
@ -170,9 +170,10 @@ class Ui_MainWindow(object):
self.label.setText(_translate("MainWindow", "Auditioner MIDI Input"))
self.auditionerCurrentInstrument_label.setText(_translate("MainWindow", "TextLabel"))
self.instruments_treeWidget.headerItem().setText(1, _translate("MainWindow", "ID"))
self.instruments_treeWidget.headerItem().setText(2, _translate("MainWindow", "Name"))
self.instruments_treeWidget.headerItem().setText(3, _translate("MainWindow", "Variant"))
self.instruments_treeWidget.headerItem().setText(4, _translate("MainWindow", "Tags"))
self.instruments_treeWidget.headerItem().setText(2, _translate("MainWindow", "Volume"))
self.instruments_treeWidget.headerItem().setText(3, _translate("MainWindow", "Name"))
self.instruments_treeWidget.headerItem().setText(4, _translate("MainWindow", "Variant"))
self.instruments_treeWidget.headerItem().setText(5, _translate("MainWindow", "Tags"))
self.iinstruments_tabWidget.setTabText(self.iinstruments_tabWidget.indexOf(self.Instruments), _translate("MainWindow", "Instruments"))
self.mixerInstructionLabel.setText(_translate("MainWindow", "This mixer controls the amount of each loaded instrument in the optional mixer output-ports. Idividual instrument outputs are unaffected."))
self.iinstruments_tabWidget.setTabText(self.iinstruments_tabWidget.indexOf(self.Mixer), _translate("MainWindow", "Mixer"))

13
qtgui/designer/mainwindow.ui

@ -183,6 +183,11 @@
<string>ID</string>
</property>
</column>
<column>
<property name="text">
<string>Volume</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
@ -251,8 +256,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>599</width>
<height>447</height>
<width>626</width>
<height>452</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
@ -329,8 +334,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>597</width>
<height>155</height>
<width>624</width>
<height>150</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">

100
qtgui/instrument.py

@ -27,13 +27,13 @@ import logging; logger = logging.getLogger(__name__); logger.info("import")
from PyQt5 import QtCore, QtGui, QtWidgets
#Our Qt
from template.qtgui.helper import ToggleSwitch
from template.qtgui.helper import ToggleSwitch,FancySwitch
#Engine
import engine.api as api
COLUMNS = ("state", "id-key", "name", "loaded", "group", "tags" ) #Loaded = Variant
COLUMNS = ("state", "id-key", "mixSend", "name", "loaded", "group", "tags" ) #Loaded = Variant
class InstrumentTreeController(object):
@ -60,8 +60,9 @@ class InstrumentTreeController(object):
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"),
@ -222,7 +223,7 @@ class InstrumentTreeController(object):
parentLibraryWidget.addChild(gi)
else:
self.treeWidget.addTopLevelItem(gi)
gi.injectToggleSwitch() #only possible after gi.init() was done and item inserted.
gi.injectWidgets() #only possible after gi.init() was done and item inserted.
self.guiInstruments[instrumentDict["id-key"]] = gi
if instrumentDict["id-key"] in self._cachedLastInstrumentStatus:
gi.updateStatus(self._cachedLastInstrumentStatus[instrumentDict["id-key"]])
@ -261,7 +262,6 @@ class InstrumentTreeController(object):
#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["id-key"]] = 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
@ -345,7 +345,27 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
self.instrumentDict = None
self._writeColumns(instrumentDict)
self.toggleSwitch = ToggleSwitch(track_radius=8,thumb_radius=7) #radius is the actual size, not the rounded corners.
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.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)
@ -374,7 +394,7 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
self.setIcon(COLUMNS.index("name"), self.offIcon)
self.activeFlag = False #midi indicator
self.midiActiveFlag = False #midi indicator
def instrumentSwitchOnViaGui(self, state):
@ -387,6 +407,7 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
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"]
@ -394,19 +415,67 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
self.setText(variantColumnIndex, instrumentStatus["currentVariant"].rstrip(".sfz")) #either "" or a variant
self.state = instrumentStatus["state"]
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)
self.mixSendDial.setValue(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)
elif not firstLoad: #and not self.state
#the instrument was once loaded and is currently connected
api.session.eventLoop.verySlowDisconnect(self._activityOff)
def injectToggleSwitch(self):
"""Call this after the item was added to the tree"""
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.
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"""
@ -416,7 +485,7 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
for index, key in enumerate(self.columns):
QtCore.QCoreApplication.translate("OpenSession", "not saved")
if key == "state" or key == "loaded":
if key == "state" or key == "loaded" or key == "mixSend":
pass #this arrives through a different api.callback
elif type(instrumentDict[key]) is str:
@ -447,11 +516,16 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
def _activityOff(self):
"""Called by a timer"""
if self.activeFlag:
self.activeFlag = False
if self.midiActiveFlag:
self.midiActiveFlag = False
self.setIcon(COLUMNS.index("name"), self.offIcon)
def activity(self):
"""Show midi note ons as flashing light."""
self.activeFlag = True
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)

28
qtgui/mainwindow.py

@ -21,7 +21,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging; logging.info("import {}".format(__file__))
#Standard Library Modules
import os.path
#Third Party Modules
from PyQt5 import QtWidgets, QtCore, QtGui
@ -33,11 +33,14 @@ from template.qtgui.about import About
#Our modules
import engine.api as api
from engine.config import * #imports METADATA
from .instrument import InstrumentTreeController
from .auditioner import AuditionerMidiInputComboController
from .selectedinstrumentcontroller import SelectedInstrumentController
from .chooseDownloadDirectory import ChooseDownloadDirectory
class MainWindow(TemplateMainWindow):
def __init__(self):
@ -56,6 +59,7 @@ class MainWindow(TemplateMainWindow):
#Do not start them all with "You can..." or "...that you can", in response to the Did you know? title.
#We use injection into the class and not a parameter because this dialog gets shown by creating an object. We can't give the parameters when this is shown via the mainWindow menu.
About.didYouKnow = [
QtCore.QCoreApplication.translate("About", "Each instrument has individual JACK audio outputs, but also is send to an internal stereo mixer output. The instruments mixer-volume can be changed, which has no effect on the individual output."),
QtCore.QCoreApplication.translate("About", "Double click an instrument to load it into the special Auditioner slot."),
QtCore.QCoreApplication.translate("About", "There is no way to load your own instruments into this program. If you create your own instruments and would like them to be included please contact the developers for a collaboration.")
] + About.didYouKnow
@ -74,9 +78,28 @@ class MainWindow(TemplateMainWindow):
self.instrumentTreeController = InstrumentTreeController(parentMainWindow=self)
self.selectedInstrumentController = SelectedInstrumentController(parentMainWindow=self)
self.tabWidget = self.ui.iinstruments_tabWidget #with this ugly name it is only a matter of time that it gets changed
self.tabWidget.setTabBarAutoHide(True)
self.tabWidget.setTabVisible(1, False) #Hide Mixer until we decide if we need it. #TODO
self.ui.search_groupBox.setVisible(False)
self.setupMenu()
self.start() #This shows the GUI, or not, depends on the NSM gui save setting. We need to call that after the menu, otherwise the about dialog will block and then we get new menu entries, which looks strange.
#Find out if we already have a global sample directory
additionalData={}
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
if settings.contains("sampleDownloadDirectory"):
additionalData["baseSamplePath"] = settings.value("sampleDownloadDirectory", type=str)
else: #first start.
dialog = ChooseDownloadDirectory()
if dialog.path:
additionalData["baseSamplePath"] = dialog.path
else:
additionalData["baseSamplePath"] = "/tmp" #TODO: At least give a message.
self.start(additionalData) #This shows the GUI, or not, depends on the NSM gui save setting. We need to call that after the menu, otherwise the about dialog will block and then we get new menu entries, which looks strange.
#Statusbar will show possible actions, such as "use scrollwheel to transpose"
#self.statusBar().showMessage(QtCore.QCoreApplication.translate("Statusbar", ""))
@ -88,6 +111,7 @@ class MainWindow(TemplateMainWindow):
#New menu entries and template-menu overrides
#self.menu.connectMenuEntry("actionAbout", lambda: print("About Dialog Menu deactivated")) #deactivates the original function
#self.menu.addMenuEntry("menuEdit", "actionNils", "Nils", lambda: print("Merle"))
self.menu.addMenuEntry("menuEdit", "actionSampleDirPathDialog", "Sample Files Location", ChooseDownloadDirectory)
self.menu.addMenuEntry("menuEdit", "actionLoadSamples", QtCore.QCoreApplication.translate("Menu", "Load all Instrument Samples (slow!)"), api.loadAllInstrumentSamples)
#self.menu.connectMenuEntry("actionNils", lambda: print("Override"))
self.menu.addSubmenu("menuView", QtCore.QCoreApplication.translate("Menu", "View"))

5
template/qtgui/helper.py

@ -175,6 +175,7 @@ def setPaletteAndFont(qtApp):
qtApp.setFont(font)
return fPalBlue
class ToggleSwitch(QtWidgets.QAbstractButton):
"""From https://stackoverflow.com/questions/14780517/toggle-switch-in-qt/38102598
@ -338,3 +339,7 @@ class ToggleSwitch(QtWidgets.QAbstractButton):
def enterEvent(self, event): # pylint: disable=invalid-name
self.setCursor(QtCore.Qt.PointingHandCursor)
super().enterEvent(event)
class FancySwitch(ToggleSwitch):
pass

8
template/qtgui/mainwindow.py

@ -134,11 +134,15 @@ class MainWindow(QtWidgets.QMainWindow):
self.initiGuiSharedDataToSave()
def start(self):
def start(self, additionalData:dict=None):
api.session.eventLoop.start() #The event loop must be started after the qt app
api.session.eventLoop.fastConnect(self.nsmClient.reactToMessage)
api.startEngine(self.nsmClient) #Load the file, start the eventLoop. Triggers all the callbacks that makes us draw.
if additionalData: #e.g. tembro global sample directory
api.startEngine(self.nsmClient, additionalData) #Load the file, start the eventLoop. Triggers all the callbacks that makes us draw.
else:
api.startEngine(self.nsmClient) #Load the file, start the eventLoop. Triggers all the callbacks that makes us draw.
if api.session.guiWasSavedAsNSMVisible:
self.showGUI()

Loading…
Cancel
Save