#! /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 . """ import logging; logger = logging.getLogger(__name__); logger.info("import") from 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) #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""" 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.