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.
356 lines
18 KiB
356 lines
18 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 Modules
|
|
import pathlib
|
|
import os
|
|
import sys
|
|
|
|
|
|
#Third Party Modules
|
|
from PyQt5 import QtWidgets, QtCore, QtGui
|
|
|
|
#Template Modules
|
|
from template.qtgui.mainwindow import MainWindow as TemplateMainWindow
|
|
from template.qtgui.menu import Menu
|
|
from template.qtgui.about import About
|
|
|
|
#Our modules
|
|
import engine.api as api
|
|
from engine.config import * #imports METADATA
|
|
|
|
from .instrument import InstrumentTreeController
|
|
from .favorites import FavoritesTreeController
|
|
from .auditioner import AuditionerMidiInputComboController
|
|
from .selectedinstrumentcontroller import SelectedInstrumentController
|
|
from .verticalpiano import VerticalPiano
|
|
from .horizontalpiano import HorizontalPiano
|
|
from .chooseDownloadDirectory import ChooseDownloadDirectory
|
|
|
|
#Template Modules
|
|
from template.calfbox import cbox
|
|
|
|
if "fake" in str(cbox):
|
|
logger.error("--mute is not supported in Tembro. We are sorry for the inconvenience.")
|
|
#We can quit here safely. Neither (fake) nsm nor anything related to audio is started.
|
|
sys.exit()
|
|
|
|
|
|
class MainWindow(TemplateMainWindow):
|
|
|
|
def __init__(self):
|
|
"""The order of calls is very important.
|
|
The split ploint is calling the super.__init. Some functions need to be called before,
|
|
some after.
|
|
For example:
|
|
|
|
The about dialog is created in the template main window init. So we need to set additional
|
|
help texts before that init.
|
|
"""
|
|
|
|
#Inject more help texts in the templates About "Did You Know" field.
|
|
#About.didYouKnow is a class variable.
|
|
#Make the first three words matter!
|
|
#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
|
|
|
|
super().__init__()
|
|
|
|
#To prevent double counting of instruments loads we remember what we already loaded this time. The permanent database is in the qt settings.
|
|
self.favoriteInstrumentsThisRun = set() #idKey Tuple
|
|
|
|
#Make the description field at least a bit visible
|
|
self.ui.details_groupBox.setMinimumSize(1, 50)
|
|
self.ui.splitter.setSizes([1,1]) #just a forced update
|
|
|
|
self.auditionerMidiInputComboController = AuditionerMidiInputComboController(parentMainWindow=self)
|
|
self.instrumentTreeController = InstrumentTreeController(parentMainWindow=self, treeWidget=self.ui.instruments_treeWidget)
|
|
self.favoritesTreeController = FavoritesTreeController(parentMainWindow=self, treeWidget=self.ui.favorites_treeWidget)
|
|
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. Better save in a permanent var.
|
|
self.tabWidget.setTabBarAutoHide(True)
|
|
#self.tabWidget.setTabVisible(2, False) #Hide Favorites until we decide if we need it.
|
|
#The tab widget / treewidget combination triggers a bug in Qt. The treewidget will actively switch to the wrongly selected item.
|
|
#We added a signal on tabChanged to set it back as a hack/workaround. Remember the real item here:
|
|
self.rememberCurrentItem = None #idKey
|
|
self.duringTabChange = False #our own "block signal"
|
|
self.tabWidget.tabBarClicked.connect(self.signalBlockTabAboutToChange)
|
|
self.tabWidget.currentChanged.connect(self.bugWorkaroundRestoreCurrentItem)
|
|
|
|
#Set up the two pianos
|
|
self.verticalPiano = VerticalPiano(self)
|
|
self.ui.verticalPianoFrame.layout().addWidget(self.verticalPiano) #add to DesignerUi. Layout is empty
|
|
self.ui.verticalPianoFrame.setFixedWidth(150)
|
|
|
|
self.horizontalPiano = HorizontalPiano(self)
|
|
self.ui.horizontalPianoFrame.layout().addWidget(self.horizontalPiano) #add to DesignerUi. Layout is empty
|
|
self.ui.horizontalPianoFrame.setFixedHeight(150)
|
|
|
|
style = """
|
|
QScrollBar:horizontal {
|
|
border: 1px solid black;
|
|
}
|
|
|
|
QScrollBar::handle:horizontal {
|
|
background: #00b2b2;
|
|
}
|
|
|
|
QScrollBar:vertical {
|
|
border: 1px solid black;
|
|
}
|
|
|
|
QScrollBar::handle:vertical {
|
|
background: #00b2b2;
|
|
}
|
|
|
|
"""
|
|
#self.setStyleSheet(style)
|
|
|
|
self.setupMenu()
|
|
|
|
#Find out if we already have a global sample directory
|
|
additionalData={}
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
|
|
if settings.contains("sampleDownloadDirectory"):
|
|
sampleDir = pathlib.Path(settings.value("sampleDownloadDirectory", type=str))
|
|
if sampleDir.exists() and sampleDir.is_dir() and os.access(settings.value("sampleDownloadDirectory", type=str), os.R_OK): #readable?
|
|
sampleDirOk = True
|
|
else:
|
|
sampleDirOk = False
|
|
else:
|
|
sampleDirOk = False
|
|
|
|
if sampleDirOk:
|
|
additionalData["baseSamplePath"] = settings.value("sampleDownloadDirectory", type=str)
|
|
else: #first start.
|
|
dialog = ChooseDownloadDirectory(parentMainWindow=self, autoStartOnFirstRun=True)
|
|
if dialog.path:
|
|
#It is possible that a download has happened at this point and we have a valid sample dir
|
|
#Or we have a valid, but empty sample dir.
|
|
#But it is also possible that there is garbage in the directory input field and the user just clicked ok.
|
|
sampleDir = pathlib.Path(dialog.path)
|
|
if sampleDir.exists() and sampleDir.is_dir() and os.access(dialog.path, os.R_OK): #readable?
|
|
additionalData["baseSamplePath"] = dialog.path
|
|
else:
|
|
additionalData["baseSamplePath"] = "/tmp"
|
|
else:
|
|
additionalData["baseSamplePath"] = "/tmp"
|
|
|
|
if settings.contains("pianoRollVisible"):
|
|
self.pianoRollToggleVisibleAndRemember(settings.value("pianoRollVisible", type=bool))
|
|
if settings.contains("pianoVisible"):
|
|
self.pianoToggleVisibleAndRemember(settings.value("pianoVisible", type=bool))
|
|
if settings.contains("instrumentTreeIsNested"):
|
|
self.ui.actionFlatNested.setChecked(settings.value("instrumentTreeIsNested", type=bool))
|
|
if settings.contains("showOnlyLoadedInstruments"):
|
|
self.ui.actionShowOnlyLoadedInstruments.setChecked(settings.value("showOnlyLoadedInstruments", type=bool))
|
|
|
|
autoconnectMixer = False
|
|
if api.isStandaloneMode() and settings.contains("autoconnectMixer"):
|
|
autoconnectMixer = settings.value("autoconnectMixer", type=bool)
|
|
additionalData["autoconnectMixer"] = autoconnectMixer
|
|
|
|
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.
|
|
|
|
api.callbacks.rescanSampleDir.append(self.react_rescanSampleDir) #This only happens on actual, manually instructed rescanning through the api. We instruct this through our Rescan-Dialog.
|
|
|
|
#Statusbar will show possible actions, such as "use scrollwheel to transpose"
|
|
#self.statusBar().showMessage(QtCore.QCoreApplication.translate("Statusbar", ""))
|
|
self.statusBar().showMessage("")
|
|
|
|
|
|
def setupMenu(self):
|
|
"""In its own function purely for readability
|
|
This is called before loading saved values for widget visibility etc.
|
|
These are the defaults."""
|
|
#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", "Instrument Location and Download", lambda: ChooseDownloadDirectory(parentMainWindow=self))
|
|
self.menu.addMenuEntry("menuEdit", "actionLoadSamples", QtCore.QCoreApplication.translate("Menu", "Load all Instrument Samples (slow!)"), api.loadAllInstrumentSamples)
|
|
self.menu.addMenuEntry("menuEdit", "actionUnloadSamples", QtCore.QCoreApplication.translate("Menu", "Unload all Instrument Samples (also slow.)"), api.unloadAllInstrumentSamples)
|
|
|
|
self.menu.addSeparator("menuEdit")
|
|
self.menu.addMenuEntry("menuEdit", "actionResetFavoriteInstrumentDatabase", QtCore.QCoreApplication.translate("Menu", "Reset favorite instrument list"), self.resetFavoriteInstrumentDatabase)
|
|
|
|
#self.menu.connectMenuEntry("actionNils", lambda: print("Override"))
|
|
self.menu.addSubmenu("menuView", QtCore.QCoreApplication.translate("Menu", "View"))
|
|
|
|
self.menu.addMenuEntry("menuView", "actionExpandAll", QtCore.QCoreApplication.translate("Menu", "Expand all Libraries"), lambda: self.instrumentTreeController.setAllExpanded(True))
|
|
self.menu.addMenuEntry("menuView", "actionCollapseAll", QtCore.QCoreApplication.translate("Menu", "Collapse all Libraries"), lambda: self.instrumentTreeController.setAllExpanded(False))
|
|
|
|
self.menu.addMenuEntry("menuView", "actionShowOnlyLoadedInstruments", QtCore.QCoreApplication.translate("Menu", "Show only loaded instruments"), self.toggleShowOnlyLoadedInstrumentsAndRemember, checkable=True, startChecked=False) #function receives check state as automatic parameter
|
|
self.menu.addMenuEntry("menuView", "actionFlatNested", QtCore.QCoreApplication.translate("Menu", "Nested Instrument List"), self.toggleNestedFlatAndRemember, checkable=True, startChecked=True) #function receives check state as automatic parameter
|
|
self.menu.addMenuEntry("menuView", "actionPianoRollVisible", QtCore.QCoreApplication.translate("Menu", "Piano Roll"), self.pianoRollToggleVisibleAndRemember, shortcut="Ctrl+R", checkable=True, startChecked=True) #function receives check state as automatic parameter
|
|
self.menu.addMenuEntry("menuView", "actionPianoVisible", QtCore.QCoreApplication.translate("Menu", "Piano"), self.pianoToggleVisibleAndRemember, shortcut="Ctrl+P", checkable=True, startChecked=True) #function receives check state as automatic parameter
|
|
|
|
if api.isStandaloneMode():
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
autoconnectMixer = settings.value("autoconnectMixer", type=bool) if settings.contains("autoconnectMixer") else False
|
|
|
|
self.menu.addSubmenu("menuSettings", QtCore.QCoreApplication.translate("mainWindow", "Settings"))
|
|
self.menu.addMenuEntry(
|
|
submenu = "menuSettings",
|
|
actionAsString = "actionAutoconnectMixer",
|
|
text = QtCore.QCoreApplication.translate("Menu", "Autoconnect Mixer and Auditioner ports"),
|
|
connectedFunction = self.react_autoconnectMixerCheckbox,
|
|
tooltip = QtCore.QCoreApplication.translate("Menu", "Wether to autoconnect the mixer and auditioner ports on program start. Not for NSM."),
|
|
checkable=True,
|
|
startChecked=autoconnectMixer,
|
|
)
|
|
self.menu.orderSubmenus(["menuFile", "menuEdit", "menuView", "menuSettings", "menuHelp"])
|
|
else:
|
|
self.menu.orderSubmenus(["menuFile", "menuEdit", "menuView", "menuHelp"])
|
|
|
|
def react_rescanSampleDir(self):
|
|
"""instructs the GUI to forget all cached data and start fresh.
|
|
Pure signal without parameters.
|
|
|
|
This only happens on actual, manually instructed rescanning through the api.
|
|
The program start happens without that and just sends data into a prepared but empty GUI."""
|
|
self.instrumentTreeController.reset()
|
|
|
|
def react_autoconnectMixerCheckbox(self, state:bool):
|
|
assert api.isStandaloneMode()
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
settings.setValue("autoconnectMixer", state)
|
|
if state:
|
|
api.connectMixerToSystemPorts()
|
|
|
|
def toggleShowOnlyLoadedInstrumentsAndRemember(self, state:bool):
|
|
"""
|
|
This function can only be called after the instrument tree has been build.
|
|
"""
|
|
self.ui.actionShowOnlyLoadedInstruments.setChecked(state) #if called from outside the menu, e.g. load
|
|
self.instrumentTreeController.buildTree(data=None) #with data=None it will used the cache data we received once, at startup. All other values use the menu check marks
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
settings.setValue("showOnlyLoadedInstruments", state)
|
|
|
|
|
|
def toggleNestedFlatAndRemember(self, state:bool):
|
|
"""We receive state as automatic parameter from Qt from the calling menu action.
|
|
|
|
This function can only be called after the instrument tree has been build.
|
|
instrumentTreeController.buildTree(data=None) relies on cached data.
|
|
"""
|
|
self.ui.actionFlatNested.setChecked(state) #if called from outside the menu, e.g. load
|
|
self.instrumentTreeController.buildTree(data=None) #with data=None it will used the cache data we received once, at startup. All other values use the menu check marks
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
settings.setValue("instrumentTreeIsNested", state)
|
|
|
|
|
|
def pianoRollToggleVisibleAndRemember(self, state:bool):
|
|
self.ui.actionPianoRollVisible.setChecked(state) #if called from outside the menu, e.g. load
|
|
self.ui.verticalPianoFrame.setVisible(state)
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
settings.setValue("pianoRollVisible", state)
|
|
|
|
|
|
def pianoToggleVisibleAndRemember(self, state:bool):
|
|
self.ui.actionPianoVisible.setChecked(state) #if called from outside the menu, e.g. load
|
|
self.ui.horizontalPianoFrame.setVisible(state)
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
settings.setValue("pianoVisible", state)
|
|
|
|
|
|
def currentTreeItemChanged(self, currentTreeItem:QtWidgets.QTreeWidgetItem, previousTreeItem:QtWidgets.QTreeWidgetItem):
|
|
"""Somewhere in the program the user-selected tree item changed.
|
|
It can be an instrument or a library. Send this change to all widgets
|
|
that deal with instruments and libraries, which are most of them.
|
|
|
|
The widgets handle the type of item themselves. We attached all engine data
|
|
to the items themselves. The items are not universal for the whole program but children
|
|
of one specific widget. The receivers of this callback need to extract their information
|
|
from the treeItem.
|
|
|
|
Technically this is the signal QTreeWidget.currentTreeItemChanged so we receive their
|
|
parameters.
|
|
"""
|
|
if self.duringTabChange:
|
|
currentTreeItem = self.rememberCurrentItem #There is a bug in tabChange signal. The TreeWidget will send an old item
|
|
|
|
self.rememberCurrentItem = currentTreeItem
|
|
|
|
if not currentTreeItem:
|
|
return
|
|
|
|
widgets = (
|
|
self.verticalPiano,
|
|
self.horizontalPiano,
|
|
self.selectedInstrumentController,
|
|
self.instrumentTreeController,
|
|
self.favoritesTreeController,
|
|
)
|
|
|
|
for widget in widgets:
|
|
widget.currentTreeItemChanged(currentTreeItem)
|
|
|
|
def signalBlockTabAboutToChange(self, tabIndex:int):
|
|
"""Click on a tab.
|
|
Finalized through bugWorkaroundRestoreCurrentItem"""
|
|
self.duringTabChange = True
|
|
|
|
def bugWorkaroundRestoreCurrentItem(self):
|
|
"""The tab already has changed.
|
|
Preceded by signalBlockTabAboutToChange"""
|
|
self.duringTabChange = False
|
|
|
|
def zoom(self, scaleFactor:float):
|
|
pass
|
|
def stretchXCoordinates(self, factor):
|
|
pass
|
|
|
|
def favoriteInstrument(self, idKey:tuple):
|
|
"""Count the user-instructed, explicit loading of an instrument in the permanent database.
|
|
We work directly with the qt settings without local data. A crash or sigkill will not
|
|
reset the counting."""
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
if settings.contains("favoriteInstruments"):
|
|
database = settings.value("favoriteInstruments", type=dict)
|
|
else:
|
|
database = {}
|
|
|
|
if not idKey in self.favoriteInstrumentsThisRun:
|
|
if not idKey in database:
|
|
database[idKey] = 0
|
|
database[idKey] += 1
|
|
self.favoriteInstrumentsThisRun.add(idKey)
|
|
|
|
settings.setValue("favoriteInstruments", database)
|
|
|
|
self.favoritesTreeController.buildTree(data=None) #rebuild from cache
|
|
|
|
|
|
def resetFavoriteInstrumentDatabase(self):
|
|
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
|
|
if settings.contains("favoriteInstruments"):
|
|
settings.remove("favoriteInstruments")
|
|
self.favoriteInstrumentsThisRun = set()
|
|
self.favoritesTreeController.buildTree(data=None) #rebuild from cache
|
|
|