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.
198 lines
7.9 KiB
198 lines
7.9 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 ),
|
|
|
|
Laborejo2 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")
|
|
|
|
from template.calfbox import cbox
|
|
|
|
from typing import Iterable, Callable, Tuple
|
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
|
import engine.api as api
|
|
from qtgui.constantsAndConfigs import constantsAndConfigs #Client constantsAndConfigs!
|
|
|
|
|
|
"""
|
|
There are two types of submenus in this file. The majority is created in menu.py during start up.
|
|
Like Clef, KeySig etc. These don't need to ask for any dynamic values.
|
|
|
|
The other is like SecondaryTempoChangeMenu. In menu.py this is bound with a lambda construct so a
|
|
new instance gets created each time the action is called by the user. Thats why this function has
|
|
self.__call__ in its init.
|
|
"""
|
|
|
|
|
|
class Submenu(QtWidgets.QDialog):
|
|
def __init__(self, mainWindow, labelString, hasOkCancelButtons=False):
|
|
super().__init__(mainWindow) #if you don't set the parent to the main window the whole screen will be the root and the dialog pops up in the middle of it.
|
|
#self.setModal(True) #we don't need this when called with self.exec() instead of self.show()
|
|
self.layout = QtWidgets.QFormLayout()
|
|
#self.layout = QtWidgets.QVBoxLayout()
|
|
self.setLayout(self.layout)
|
|
|
|
label = QtWidgets.QLabel(labelString) #"Choose a clef" or so.
|
|
self.layout.addWidget(label)
|
|
|
|
#Second Label that can be changed on every call with self.dynamicLabel.setText()
|
|
self.dynamicLabel = QtWidgets.QLabel("")
|
|
self.layout.addWidget(self.dynamicLabel)
|
|
|
|
#self.setFocus(); #self.grabKeyboard(); #redundant for a proper modal dialog. Leave here for documentation reasons.
|
|
|
|
if hasOkCancelButtons == 1: #or true
|
|
self.buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
|
|
self.buttonBox.accepted.connect(self.process)
|
|
self.buttonBox.rejected.connect(self.reject)
|
|
elif hasOkCancelButtons == 2: #only cancel. #TODO: unpythonic.
|
|
self.buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Cancel)
|
|
self.buttonBox.rejected.connect(self.reject)
|
|
else:
|
|
self.buttonBox = None
|
|
|
|
|
|
def keyPressEvent(self, event):
|
|
"""Escape closes the dialog by default.
|
|
We want Enter as "accept value"
|
|
All other methods of mixing editing, window focus and signals
|
|
results in strange qt behaviour, triggering the api function twice or more.
|
|
Especially unitbox.editingFinished is too easy to trigger.
|
|
|
|
The key-event method turned out to be the most straightforward way."""
|
|
try:
|
|
getattr(self, "process")
|
|
k = event.key() #49=1, 50=2 etc.
|
|
if k == 0x01000004 or k == 0x01000005: #normal enter or keypad enter
|
|
event.ignore()
|
|
self.process()
|
|
else: #Pressed Esc
|
|
self.abortHandler()
|
|
super().keyPressEvent(event)
|
|
except AttributeError:
|
|
super().keyPressEvent(event)
|
|
|
|
def showEvent(self, event):
|
|
#TODO: not optimal but better than nothing.
|
|
super().showEvent(event)
|
|
#self.resize(self.layout.geometry().width(), self.layout.geometry().height())
|
|
self.resize(self.childrenRect().height(), self.childrenRect().width())
|
|
self.updateGeometry()
|
|
|
|
def abortHandler(self):
|
|
pass
|
|
|
|
def process(self):
|
|
"""Careful! Calling this eats python errors without notice. Make sure your objects exists
|
|
and your syntax is correct"""
|
|
raise NotImplementedError()
|
|
self.done(True)
|
|
|
|
def __call__(self):
|
|
"""This instance can be called like a function.
|
|
Subclasses can subclass this as well to create non-static functionality."""
|
|
if self.buttonBox:
|
|
self.layout.addWidget(self.buttonBox)
|
|
self.setFixedSize(self.layout.geometry().size())
|
|
self.exec() #blocks until the dialog gets closed
|
|
|
|
"""
|
|
Most submenus have the line "lambda, r, value=value"...
|
|
the r is the return value we get automatically from the Qt buttons which need to be handled.
|
|
"""
|
|
|
|
|
|
class ChooseOne(Submenu):
|
|
"""A generic submenu that presents a list of options to the users.
|
|
Only supports up to ten entries, for number shortcuts"""
|
|
def __init__(self, mainWindow, title:str, lst:Iterable[Tuple[str, Callable]]):
|
|
if len(lst) > 9:
|
|
raise ValueError(f"ChooseOne submenu supports up to nine entries. You have {len(lst)}")
|
|
super().__init__(mainWindow, title)
|
|
|
|
for number, (prettyname, function) in enumerate(lst):
|
|
button = QtWidgets.QPushButton(f"[{number+1}] {prettyname}")
|
|
button.setShortcut(QtGui.QKeySequence(str(number+1)))
|
|
button.setStyleSheet("Text-align:left; padding: 5px;");
|
|
self.layout.addWidget(button)
|
|
button.clicked.connect(function)
|
|
button.clicked.connect(self.done)
|
|
|
|
|
|
#TODO: This breaks backend/frontend division. We ask calfbox/jack directly.
|
|
def nestedJackPortsMenu(parseOtherOutputs:bool=True, midi:bool=True):
|
|
"""
|
|
This function queries JACK each time it is called. No cache.
|
|
|
|
The jack graph has a fixed depth of 1.
|
|
Each client gets a submenu.
|
|
|
|
The default shows all output midi ports of other programs.
|
|
If we are a sampler this enables a context menu on an instrument to connect another programs
|
|
sequencer port, or a hardware controller, to us.
|
|
|
|
set parseOtherOutputs to False to get other input ports instead. Use this if we are the sequencer
|
|
and click on a track to connect it to an external synth.
|
|
|
|
switch midi to False for Audio ports. Use this to connect our sample audio outputs (mono)
|
|
to external effects or DAW recording tracks.
|
|
|
|
ourClientPortName is the full jack name, such as:
|
|
Patroneo:BassC
|
|
"""
|
|
|
|
menu = QtWidgets.QMenu()
|
|
|
|
result = {}
|
|
hardware = set(cbox.JackIO.get_ports(".*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL))
|
|
allPorts = set(cbox.JackIO.get_ports(".*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE))
|
|
software = allPorts.difference(hardware)
|
|
result["hardware"] = sorted(list(hardware))
|
|
result["software"] = sorted(list(software))
|
|
|
|
action = menu.addAction("Disconnect")
|
|
action.setData(-1)
|
|
|
|
hardwareMenu = menu.addMenu("Hardware")
|
|
for clientAndPortString in result["hardware"]:
|
|
action = hardwareMenu.addAction(clientAndPortString)
|
|
action.setData(clientAndPortString)
|
|
|
|
softwareClean = {} #clientName:portName
|
|
clientMenus = {} #clientName:QMenu
|
|
for fullString in result["software"]:
|
|
|
|
if "a2j" in fullString:
|
|
continue
|
|
|
|
client, port = fullString.split(":", 1)
|
|
|
|
if not client in softwareClean:
|
|
softwareClean[client] = list()
|
|
clientMenus[client] = menu.addMenu(client)
|
|
|
|
softwareClean[client].append(port)
|
|
action = clientMenus[client].addAction(port)
|
|
action.setData(fullString)
|
|
|
|
|
|
pos = QtGui.QCursor.pos()
|
|
pos.setY(pos.y() + 5)
|
|
result = menu.exec_(pos)
|
|
if result: #None or QAction
|
|
return result.data() #text is just the port. data is the full string, we set this ourselves above.
|
|
|