Sampled Instrument Player with static and monolithic design. All instruments are built-in.
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.
 
 

214 lines
11 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; logging.info("import {}".format(__file__))
#Standard Library Modules
import pathlib
import os
#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 .auditioner import AuditionerMidiInputComboController
from .selectedinstrumentcontroller import SelectedInstrumentController
from .verticalpiano import VerticalPiano
from .horizontalpiano import HorizontalPiano
from .chooseDownloadDirectory import ChooseDownloadDirectory
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__()
#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)
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
#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))
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"""
#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", 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.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))
if "nestedView" in api.session.guiSharedDataToSave:
nested = api.session.guiSharedDataToSave["nestedView"]
else:
nested = True
self.menu.addMenuEntry("menuView", "actionFlatNested", QtCore.QCoreApplication.translate("Menu", "Nested Instrument List"), self.instrumentTreeController.toggleNestedFlat, checkable=True, startChecked=nested) #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=nested) #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=nested) #function receives check state as automatic parameter
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 pianoRollToggleVisibleAndRemember(self, state:bool):
self.ui.verticalPianoFrame.setVisible(state)
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
settings.setValue("pianoRollVisible", state)
self.ui.actionPianoRollVisible.setChecked(state) #if called from outside the menu, e.g. load
def pianoToggleVisibleAndRemember(self, state:bool):
self.ui.horizontalPianoFrame.setVisible(state)
settings = QtCore.QSettings("LaborejoSoftwareSuite", METADATA["shortName"])
settings.setValue("pianoVisible", state)
self.ui.actionPianoVisible.setChecked(state) #if called from outside the menu, e.g. load
def selectedInstrumentChanged(self, instrumentStatus, instrumentData):
"""We receive this from selectedinstrumentcontroller.py, when the user clicks on a GUI
entry for a different instrument. This is purely a GUI function. This functions
relays the change to other widgets, except the above mentioned controller.
If a library is clicked, and not an instrument, both parameters will be None.
The pianos use this to switch off.
"""
self.verticalPiano.pianoScene.selectedInstrumentChanged(instrumentStatus, instrumentData)
self.horizontalPiano.pianoScene.selectedInstrumentChanged(instrumentStatus, instrumentData)
def zoom(self, scaleFactor:float):
pass
def stretchXCoordinates(self, factor):
pass