#! /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 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 ) )
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.