root/trunk/libffado/support/mixer-qt4/ffado/panelmanager.py

Revision 2803, 23.0 kB (checked in by jwoithe, 3 years ago)

Cosmetic: capitalise "L" in "Linux".

"Linux" is a proper noun so it should start with a capital letter. These
changes are almost all within comments.

This patch was originally proposed by pander on the ffado-devel mailing
list. It has been expanded to cover all similar cases to maintain
consistency throughout the source tree.

  • Property svn:mergeinfo set to
Line 
1 #
2 # Copyright (C) 2005-2008 by Pieter Palmers
3 #               2007-2008 by Arnold Krille
4 #               2013 by Philippe Carriere
5 #
6 # This file is part of FFADO
7 # FFADO = Free FireWire (pro-)audio drivers for Linux
8 #
9 # FFADO is based upon FreeBoB.
10 #
11 # This program is free software: you can redistribute it and/or modify
12 # it under the terms of the GNU General Public License as published by
13 # the Free Software Foundation, either version 3 of the License, or
14 # (at your option) any later version.
15 #
16 # This program is distributed in the hope that it will be useful,
17 # but WITHOUT ANY WARRANTY; without even the implied warranty of
18 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19 # GNU General Public License for more details.
20 #
21 # You should have received a copy of the GNU General Public License
22 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
23 #
24
25 from ffado.config import * #FFADO_VERSION, FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH
26
27 # from PyQt4.QtGui import QFrame, QWidget, QTabWidget, QVBoxLayout, QMainWindow, QIcon, QAction, qApp, QStyleOptionTabWidgetFrame, QFileDialog
28 # from PyQt4.QtCore import QTimer, pyqtSignal
29 from ffado.import_pyqt import *
30
31 from ffado.dbus_util import *
32 from ffado.registration import *
33
34 from ffado.configuration import *
35
36 from ffado.mixer.globalmixer import GlobalMixer
37 from ffado.mixer.dummy import Dummy
38
39 import sys
40 import time
41 import importlib
42
43 import logging
44 log = logging.getLogger('panelmanager')
45
46 use_generic = False
47 try:
48     from mixer_generic import *
49     log.info("The generic mixer is found, seems to be a developer using ffadomixer...")
50 except ImportError:
51     pass
52 else:
53     use_generic = True
54
55 # pseudo-guid
56 GUID_GENERIC_MIXER = 0
57
58 FILE_VERSION = '0.1'
59
60 class HLine( QFrame ):
61     def __init__( self, parent ):
62         QFrame.__init__( self, parent )
63         self.setFrameShape( QFrame.HLine )
64         self.setLineWidth( 2 )
65         self.setMinimumHeight( 10 )
66
67 class PanelManagerStatus(QWidget):
68     def __init__(self, parent):
69         QWidget.__init__(self,parent)
70         uicLoad("ffado/panelmanagerstatus", self)
71
72 class OwnTabWidget(QTabWidget):
73     def __init__(self,parent):
74         QTabWidget.__init__(self,parent)
75
76     def tabInserted(self,index):
77         self.checkTabBar()
78
79     def tabRemoved(self,index):
80         self.checkTabBar()
81
82     def checkTabBar(self):
83         if self.count()<2:
84             self.tabBar().hide()
85         else:
86             self.tabBar().show()
87
88 class PanelManager(QWidget):
89     connectionLost = pyqtSignal(name='connectionLost')
90     def __init__(self, parent, devmgr=None):
91         QWidget.__init__(self, parent)
92         self.setObjectName("PanelManager")
93         self.parent = parent
94
95         # maps a device GUID to a QT panel
96         self.panels = {}
97
98         # a layout for ourselves
99         self.layout = QVBoxLayout(self)
100
101         # the tabs
102         self.tabs = OwnTabWidget(self)
103         self.tabs.hide()
104         self.layout.addWidget(self.tabs)
105
106         # a dialog that is shown during update
107         self.status = PanelManagerStatus(self)
108         self.layout.addWidget(self.status)
109         self.status.show()
110
111         self.devices = DeviceList( SYSTEM_CONFIG_FILE )
112         self.devices.updateFromFile( USER_CONFIG_FILE )
113
114         if devmgr is not None:
115             self.setManager(devmgr)
116
117     def __del__(self):
118         print("PanelManager.__del__()")
119         self.polltimer.stop()
120
121     def setManager(self,devmgr):
122         self.devmgr = devmgr
123         self.devmgr.registerPreUpdateCallback(self.devlistPreUpdate)
124         self.devmgr.registerPostUpdateCallback(self.devlistPostUpdate)
125         self.devmgr.registerUpdateCallback(self.devlistUpdate)
126         self.devmgr.registerDestroyedCallback(self.devmgrDestroyed)
127         # create a timer to poll the panels
128         self.polltimer = QTimer()
129         self.polltimer.timeout.connect(self.pollPanels)
130         self.polltimer.start( POLL_SLEEP_TIME_MSEC )
131
132         # create a timer to initialize the panel after the main form is shown
133         # since initialization can take a while
134         QTimer.singleShot( POLL_SLEEP_TIME_MSEC, self.updatePanels )
135
136         # live check timer
137         self.alivetimer = QTimer()
138         self.alivetimer.timeout.connect(self.commCheck)
139         self.alivetimer.start( 2000 )
140
141     def count(self):
142         return self.tabs.count()
143
144     def pollPanels(self):
145         #log.debug("PanelManager::pollPanels()")
146         # only when not modifying the tabs
147         try:
148             if self.tabs.isEnabled():
149                 for guid in self.panels.keys():
150                     w = self.panels[guid]
151                     for child in w.children():
152                         #log.debug("poll child %s,%s" % (guid,child))
153                         if 'polledUpdate' in dir(child):
154                             child.polledUpdate()
155         except:
156             log.error("error in pollPanels")
157             self.commCheck()
158
159     def devlistPreUpdate(self):
160         log.debug("devlistPreUpdate")
161         self.tabs.setEnabled(False)
162         self.tabs.hide()
163         self.status.lblMessage.setText("Bus reconfiguration in progress, please wait...")
164         self.status.show()
165         #self.statusBar().showMessage("bus reconfiguration in progress...", 5000)
166
167     def devlistPostUpdate(self):
168         log.debug("devlistPostUpdate")
169         # this can fail if multiple busresets happen in fast succession
170         ntries = 10
171         while ntries > 0:
172             try:
173                 self.updatePanels()
174                 return
175             except:
176                 log.debug("devlistPostUpdate failed (%d)" % ntries)
177                 for guid in self.panels.keys():
178                     w = self.panels[guid]
179                     del self.panels[guid] # remove from the list
180                     idx = self.tabs.indexOf(w)
181                     self.tabs.removeTab(idx)
182                     del w # GC might also take care of that
183
184                 ntries = ntries - 1
185                 time.sleep(2) # sleep a few seconds
186
187         log.debug("devlistPostUpdate failed completely")
188         self.tabs.setEnabled(False)
189         self.tabs.hide()
190         self.status.lblMessage.setText("Error while reconfiguring. Please restart ffadomixer.")
191         self.status.show()
192
193
194     def devlistUpdate(self):
195         log.debug("devlistUpdate")
196
197     def devmgrDestroyed(self):
198         log.debug("devmgrDestroyed")
199         self.alivetimer.stop()
200         self.tabs.setEnabled(False)
201         self.tabs.hide()
202         self.status.lblMessage.setText("DBUS server was shut down, please restart it and restart ffadomixer...")
203         self.status.show()
204
205     def commCheck(self):
206         try:
207             nbDevices = self.devmgr.getNbDevices()
208         except:
209             log.error("The communication with ffado-dbus-server was lost.")
210             self.tabs.setEnabled(False)
211             self.polltimer.stop()
212             self.alivetimer.stop()
213             keys = self.panels.keys()
214             for panel in keys:
215                 w = self.panels[panel]
216                 del self.panels[panel]
217                 w.deleteLater()
218             self.connectionLost.emit()
219
220     def removePanel(self, guid):
221         print( "Removing widget for device" + guid )
222         w = self.panels[guid]
223         del self.panels[guid] # remove from the list
224         idx = self.tabs.indexOf(w)
225         self.tabs.removeTab(idx)
226         w.deleteLater()
227         self.parent.editmenu.removeAction(self.parent.devices[guid])
228         self.parent.devices.pop(guid, None)
229
230     def addPanel(self, idx):
231         path = self.devmgr.getDeviceName(idx)
232         log.debug("Adding device %d: %s" % (idx, path))
233
234         if ffado.config.bypassdbus:
235             cfgrom = ConfigRomInterface(FFADO_DBUS_SERVER, path)
236         else:
237             cfgrom = ConfigRomInterface(FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+'/DeviceManager/'+path)
238         vendorId = cfgrom.getVendorId()
239         modelId = cfgrom.getModelId()
240         unitVersion = cfgrom.getUnitVersion()
241         guid = cfgrom.getGUID()
242         vendorName = cfgrom.getVendorName()
243         modelName = cfgrom.getModelName()
244         log.debug(" Found (%s, %X, %X) %s %s" % (str(guid), vendorId, modelId, vendorName, modelName))
245
246         # check whether this has already been registered at ffado.org
247         if not ffado.config.bypassdbus:
248             # As of June 2020, don't offer the registration option as the
249             # statistics are no longer useful.  Coincidently, a move to
250             # different website infrastructure June 2020 would necessitate
251             # some work to reimplement the interfaces used by the
252             # registration process, which doesn't seem worthwhile.
253             #
254             # reg = ffado_registration(FFADO_VERSION, int(guid, 16),
255             #                              vendorId, modelId,
256             #                              vendorName, modelName)
257             # reg.check_for_registration()
258
259             # The MOTU devices use unitVersion to differentiate models.  For
260             # the moment though we don't need to know precisely which model
261             # we're using beyond it being a pre-mark3 (modelId=0) or mark3
262             # (modelId=1) device.
263             if vendorId == 0x1f2:
264                 # All MOTU devices with a unit version of 0x15 or greater are
265                 # mark3 devices
266                 if (unitVersion >= 0x15):
267                     modelId = 0x00000001
268                 else:
269                     modelId = 0x00000000
270
271             # The RME devices use unitVersion to differentiate models.
272             # Therefore in the configuration file we use the config file's
273             # modelid field to store the unit version.  As a result we must
274             # override the modelId with the unit version here so the correct
275             # configuration file entry (and hense mixer widget) is identified.
276             if vendorId == 0xa35:
277                 modelId = unitVersion;
278
279         dev = self.devices.getDeviceById( vendorId, modelId )
280
281         w = QWidget( )
282         l = QVBoxLayout( w )
283
284         # create a control object
285         hw = ControlInterface(FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+'/DeviceManager/'+path)
286         clockselect = ClockSelectInterface( FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+"/DeviceManager/"+path )
287         samplerateselect = SamplerateSelectInterface( FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+"/DeviceManager/"+path )
288         streamingstatus = StreamingStatusInterface( FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+"/DeviceManager/"+path )
289         nickname = TextInterface( FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+"/DeviceManager/"+path+"/Generic/Nickname" )
290
291         #
292         # Generic elements for all mixers follow here:
293         #
294         globalmixer = GlobalMixer( w )
295         globalmixer.configrom = cfgrom
296         globalmixer.clockselect = clockselect
297         globalmixer.samplerateselect = samplerateselect
298         globalmixer.streamingstatus = streamingstatus
299         globalmixer.nickname = nickname
300         globalmixer.hw = hw
301         globalmixer.initValues()
302         l.addWidget( globalmixer, 1 )
303
304         #
305         # Line to separate
306         #
307         l.addWidget( HLine( w ) )
308
309         #
310         # Specific (or dummy) mixer widgets get loaded in the following
311         #
312         found = False
313         if 'mixer' in dev and dev['mixer'] != None:
314             mixerapp = dev['mixer']
315             try:
316                 mixer_module = importlib.import_module("ffado.mixer.%s" % mixerapp.lower())
317                 mixerwidget = getattr(mixer_module, mixerapp)(w)
318                 found = True
319             except ImportError:
320                 log.debug("bypassdbus set, %s module not available: ignored" % mixerapp.lower())
321
322         if not found:
323             mixerwidget = Dummy( w )
324             mixerapp = modelName+" (Dummy)"
325
326         #
327         # The same for all mixers
328         #
329         l.addWidget( mixerwidget, 10 )
330         mixerwidget.configrom = cfgrom
331         mixerwidget.clockselect = clockselect
332         mixerwidget.samplerateselect = samplerateselect
333         mixerwidget.streamingstatus = streamingstatus
334         mixerwidget.nickname = nickname
335         mixerwidget.hw = hw
336         if 'buildMixer' in dir(mixerwidget):
337             mixerwidget.buildMixer()
338         if 'initValues' in dir(mixerwidget):
339             mixerwidget.initValues()
340         if 'getDisplayTitle' in dir(mixerwidget):
341             title = mixerwidget.getDisplayTitle()
342         else:
343             title = mixerapp
344
345         mixer_icon = UIDIR + '/ffado/mixer/' + vendorName.replace(" ", "_").lower() + '.png'
346         if os.path.exists(mixer_icon) :
347             globalmixer.lblName.setPixmap(QPixmap(mixer_icon))
348             globalmixer.lblName.setToolTip(title)
349             globalmixer.lblName.show()
350         else :
351             globalmixer.setName(title)
352
353         self.tabs.addTab( w, title )
354         self.panels[guid] = w
355
356         if 'onSamplerateChange' in dir(mixerwidget):
357           log.debug("Updating Mixer on samplerate change required")
358           globalmixer.onSamplerateChange = mixerwidget.onSamplerateChange
359
360         w.gmixSaveSetgs = globalmixer.saveSettings
361         w.gmixReadSetgs = globalmixer.readSettings
362         if 'saveSettings' in dir(mixerwidget):
363           w.smixSaveSetgs = mixerwidget.saveSettings
364           self.parent.saveaction.setEnabled(True)
365
366         if 'readSettings' in dir(mixerwidget):
367           w.smixReadSetgs = mixerwidget.readSettings
368           self.parent.openaction.setEnabled(True)
369
370         self.parent.devices[guid] = QAction(QIcon.fromTheme("audio-card"), str(title), self.parent)
371         # Ensure a standard type is passed to setDate() so a UserType is not
372         # adopted by the QVariant which underpins the QAction data.  "guid"
373         # is a dbus.String object; it will be stored as a UserType if passed
374         # directly, which makes it difficult to extract in setTabVisible().
375         self.parent.devices[guid].setData(str(guid))
376         self.parent.editmenu.addAction(self.parent.devices[guid])
377         self.parent.devices[guid].triggered.connect(self.setTabVisible)
378    
379     def setTabVisible(self) :
380         action = self.sender()
381         # Extract the action data and store as a dbus.String type so
382         # it is usable as a key into self.panels[].
383         panel_key = dbus.String(action.data().toString() if ffado_pyqt_version == 4 else action.data())
384         self.tabs.setCurrentIndex(self.tabs.indexOf(self.panels[panel_key]))
385
386     def displayPanels(self):
387         # if there is no panel, add the no-device message
388         # else make sure it is not present
389         if self.count() == 0:
390             self.tabs.hide()
391             self.tabs.setEnabled(False)
392             self.status.lblMessage.setText("No supported device found.")
393             self.status.show()
394             #self.statusBar().showMessage("No supported device found.", 5000)
395         else:
396             # Hide the status widget before showing the panel tab to prevent
397             # the panel manager's vertical size including that of the status
398             # widget.  For some reason, hiding the status after showing the
399             # tabs does not cause a recalculation of the panel manager's size,
400             # and the window ends up being larger than it needs to be.
401             self.status.hide()
402             self.tabs.show()
403             self.tabs.setEnabled(True)
404             #self.statusBar().showMessage("Configured the mixer for %i devices." % self.tabs.count())
405             if use_generic:
406                 #
407                 # Show the generic (development) mixer if it is available
408                 #
409                 w = GenericMixer( devmgr.bus, FFADO_DBUS_SERVER, mw )
410                 self.tabs.addTab( w, "Generic Mixer" )
411                 self.panels[GUID_GENERIC_MIXER] = w
412    
413     def updatePanels(self):
414         log.debug("PanelManager::updatePanels()")
415         nbDevices = self.devmgr.getNbDevices()
416         #self.statusBar().showMessage("Reconfiguring the mixer panels...")
417
418         # list of panels present
419         guids_with_tabs = self.panels.keys()
420
421         # build list of guids on the bus now
422         guids_present = []
423         guid_indexes = {}
424         for idx in range(nbDevices):
425             path = self.devmgr.getDeviceName(idx)
426             if ffado.config.bypassdbus:
427                 cfgrom = ConfigRomInterface(FFADO_DBUS_SERVER, path)
428             else:
429                 cfgrom = ConfigRomInterface(FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+'/DeviceManager/'+path)
430             guid = cfgrom.getGUID()
431             guids_present.append(guid)
432             guid_indexes[guid] = idx
433
434         # figure out what to remove
435         # the special panel (generic)
436         # that has (pseudo-)GUID 0
437         # is also automatically removed
438         to_remove = []
439         for guid in guids_with_tabs:
440             if not guid in guids_present:
441                 to_remove.append(guid)
442                 log.debug("going to remove %s" % str(guid))
443             else:
444                 log.debug("going to keep %s" % str(guid))
445
446         # figure out what to add
447         to_add = []
448         for guid in guids_present:
449             if not guid in guids_with_tabs:
450                 to_add.append(guid)
451                 log.debug("going to add %s" % str(guid))
452
453         # update the widget
454         for guid in to_remove:
455             self.removePanel(guid)
456
457         for guid in to_add:
458             # retrieve the device manager index
459             idx = guid_indexes[guid]
460             self.addPanel(idx)
461
462         self.displayPanels()
463
464     def refreshPanels(self):
465         log.debug("PanelManager::refreshPanels()")
466         nbDevices = self.devmgr.getNbDevices()
467         #self.statusBar().showMessage("Reconfiguring the mixer panels...")
468
469         # list of panels present
470         guids_with_tabs = self.panels.keys()
471
472         # build list of guids on the bus now
473         guid_indexes = {}
474         for idx in range(nbDevices):
475             path = self.devmgr.getDeviceName(idx)
476             cfgrom = ConfigRomInterface(FFADO_DBUS_SERVER, FFADO_DBUS_BASEPATH+'/DeviceManager/'+path)
477             guid = cfgrom.getGUID()
478             guid_indexes[guid] = idx
479
480         # remove/create the widget
481         for guid in guids_with_tabs:
482             self.removePanel(guid)
483             idx = guid_indexes[guid]
484             self.addPanel(idx)
485
486         self.displayPanels()
487
488     def saveSettings(self):
489         saveString = []
490         saveString.append('<?xml version="1.0" encoding="UTF-8"?>\n')
491         saveString.append('<fileversion>\n')
492         saveString.append('  <major>\n')
493         saveString.append('    ' + str(FILE_VERSION).split('.')[0] + '\n')
494         saveString.append('  </major>\n')
495         saveString.append('  <minor>\n')
496         saveString.append('    ' + str(FILE_VERSION).split('.')[1] + '\n')
497         saveString.append('  </minor>\n')
498         saveString.append('</fileversion>\n')
499         saveString.append('<ffadoversion>\n')
500         saveString.append('  <major>\n')
501         saveString.append('    ' + str(str(FFADO_VERSION).split('-')[0]).split('.')[0] + '\n')
502         saveString.append('  </major>\n')
503         saveString.append('  <minor>\n')
504         saveString.append('    ' + str(str(FFADO_VERSION).split('-')[0]).split('.')[1] + '\n')
505         saveString.append('  </minor>\n')
506         saveString.append('</ffadoversion>\n')
507         for guid in self.panels.keys():
508           saveString.append('<device>\n')
509           saveString.append('  <guid>\n')
510           saveString.append('    ' + str(guid) + '\n')
511           saveString.append('  </guid>\n')       
512           w = self.panels[guid]
513           indent = "  "
514           saveString.extend(w.gmixSaveSetgs(indent))
515           if 'smixSaveSetgs' in dir(w):
516               saveString.extend(w.smixSaveSetgs(indent))
517           saveString.append('</device>\n')
518         # file saving
519         savefilename = QFileDialog.getSaveFileName(self, 'Save File', os.getenv('HOME'))
520         if isinstance(savefilename, tuple): # newer PyQt5
521             savefilename = savefilename[0]
522         try:
523           f = open(savefilename, 'w')
524         except IOError:
525           print( "Unable to open save file" )
526           return
527         for s in saveString:
528           f.write(s)
529         f.close()
530
531     def readSettings(self):
532         readfilename = QFileDialog.getOpenFileName(self, 'Open File', os.getenv('HOME'))
533         if isinstance(readfilename, tuple): # newer PyQt5
534             readfilename = readfilename[0]
535         try:
536           f = open(readfilename, 'r')
537         except IOError:
538           print( "Unable to open file" )
539           return
540         log.debug("Opening file %s" % readfilename)
541         # discard useless whitespace characters
542         readString = []
543         for line in f:
544           readString.append(" ".join(str(line).split()))
545         f.close()
546         # Check it is a compatible "FFADO" file
547         # It must start with the <?xml ... tag as the first string
548         if readString[0].find("<?xml") == -1:
549             print( "Not an xml data file" )
550             return
551         # Then there must be a file version tag somewhere in the file
552         try:
553             idx = readString.index('<fileversion>')
554         except Exception:
555             print( "Data file should contain the version tag" )
556             return
557         if readString[idx+1].find("<major>") == -1:
558             print( "Incompatible versioning of the file" )
559         if readString[idx+3].find("</major>") == -1:
560             print( "Not a valid xml file" )
561         if readString[idx+4].find("<minor>") == -1:
562             print( "Incompatible versioning of the file" )
563         if readString[idx+6].find("</minor>") == -1:
564             print( "Not a valid xml file" )
565         version_major = readString[idx+2]
566         version =  version_major + '.' + readString[idx+5]
567         log.debug("File version: %s" % version)
568         # File version newer than present
569         if int(version_major) > int(str(FILE_VERSION).split('.')[0]):
570             print( "File version is too recent: you should upgrade your FFADO installation" )
571             return
572         # FIXME At a time it will be necessary to detect if an older major version is detected
573         #
574         # It looks like useless to check for the FFADO version
575         # Add something here if you would like so
576         #
577         # Now search for devices
578         nd = readString.count('<device>');
579         n  = readString.count('</device>');
580         if n != nd:
581             print( "Not a regular xml file: opening device tag must match closing ones" )
582             return
583         while nd > 0:
584           idxb = readString.index('<device>')
585           idxe = readString.index('</device>')
586           if idxe < idxb+1:
587             print( "Not a regular xml file: data must be enclosed between a <device> and </device> tag" )
588             return
589           stringDev = []
590           for s in readString[idxb:idxe]:
591             stringDev.append(s)
592           # determine the device guid
593           try:
594               idx = stringDev.index('<guid>')
595           except Exception:
596               print( "Device guid not found" )
597               return
598           guid = stringDev[idx+1]
599           log.debug("Device %s found" % guid)
600
601           if guid in self.panels:
602               w = self.panels[guid]
603               w.gmixReadSetgs(stringDev)
604               if 'smixReadSetgs' in dir(w):
605                 w.smixReadSetgs(stringDev)
606               log.debug("Settings changed for device %s" % guid)
607           else:
608               log.debug("Device %s not present; settings ignored" % guid)
609
610           del stringDev[:]
611           del readString[idxb:idxe]
612           nd -= 1
613    
614 # vim: et
Note: See TracBrowser for help on using the browser.